From 301b72a7b1882c389385e636ec53158ced092f68 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 29 Apr 2026 19:13:09 +0200 Subject: [PATCH 1/5] fix: block writing empty files with 0 quota Signed-off-by: Robin Appelman --- lib/private/Files/Storage/Wrapper/Quota.php | 21 ++++++++++--------- tests/lib/Files/Storage/Wrapper/QuotaTest.php | 6 ++++++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/private/Files/Storage/Wrapper/Quota.php b/lib/private/Files/Storage/Wrapper/Quota.php index 35a265f8c8e72..9397ba9042827 100644 --- a/lib/private/Files/Storage/Wrapper/Quota.php +++ b/lib/private/Files/Storage/Wrapper/Quota.php @@ -116,19 +116,20 @@ public function copy(string $source, string $target): bool { } public function fopen(string $path, string $mode) { - if (!$this->hasQuota()) { + if (!$this->hasQuota() || $this->isPartFile($path)) { return $this->storage->fopen($path, $mode); } - $source = $this->storage->fopen($path, $mode); - // don't apply quota for part files - if (!$this->isPartFile($path)) { - $free = $this->free_space($path); - if ($source && (is_int($free) || is_float($free)) && $free >= 0 && $mode !== 'r' && $mode !== 'rb') { - // only apply quota for files, not metadata, trash or others - if ($this->shouldApplyQuota($path)) { - return \OC\Files\Stream\Quota::wrap($source, $free); - } + $free = $this->free_space($path); + if ($this->shouldApplyQuota($path) && $free == 0) { + return false; + } + + $source = $this->getWrapperStorage()->fopen($path, $mode); + if ($source && (is_int($free) || is_float($free)) && $free >= 0 && $mode !== 'r' && $mode !== 'rb') { + // only apply quota for files, not metadata, trash or others + if ($this->shouldApplyQuota($path)) { + return \OC\Files\Stream\Quota::wrap($source, $free); } } diff --git a/tests/lib/Files/Storage/Wrapper/QuotaTest.php b/tests/lib/Files/Storage/Wrapper/QuotaTest.php index 3d313666a93b0..e3a4e6dcb5f3f 100644 --- a/tests/lib/Files/Storage/Wrapper/QuotaTest.php +++ b/tests/lib/Files/Storage/Wrapper/QuotaTest.php @@ -229,4 +229,10 @@ public function testNoTouchQuotaZero(): void { $instance = $this->getLimitedStorage(0.0); $this->assertFalse($instance->touch('foobar')); } + + public function testNoFopenQuotaZero(): void { + $instance = $this->getLimitedStorage(0.0); + $fh = $instance->fopen('files/test.txt', 'w'); + $this->assertFalse($fh); + } } From 1b5af6dbd80b9a3813d0e18075ce8b2cb3167487 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 29 Apr 2026 19:14:57 +0200 Subject: [PATCH 2/5] fix: apply quota with writeStream Signed-off-by: Robin Appelman --- lib/private/Files/Storage/Wrapper/Quota.php | 29 +++++++++++++++++++ lib/private/Files/Storage/Wrapper/Wrapper.php | 17 +++++++---- tests/lib/Files/Storage/Wrapper/QuotaTest.php | 21 ++++++++++++++ 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/lib/private/Files/Storage/Wrapper/Quota.php b/lib/private/Files/Storage/Wrapper/Quota.php index 9397ba9042827..6b9b645623aa3 100644 --- a/lib/private/Files/Storage/Wrapper/Quota.php +++ b/lib/private/Files/Storage/Wrapper/Quota.php @@ -11,6 +11,8 @@ use OC\SystemConfig; use OCP\Files\Cache\ICacheEntry; use OCP\Files\FileInfo; +use OCP\Files\GenericFileException; +use OCP\Files\NotEnoughSpaceException; use OCP\Files\Storage\IStorage; class Quota extends Wrapper { @@ -206,4 +208,31 @@ public function touch(string $path, ?int $mtime = null): bool { public function enableQuota(bool $enabled): void { $this->enabled = $enabled; } + + #[\Override] + public function writeStream(string $path, $stream, ?int $size = null): int { + if (!$this->hasQuota()) { + return parent::writeStream($path, $stream, $size); + } + + $free = $this->free_space($path); + if ($this->shouldApplyQuota($path) && $free == 0) { + throw new NotEnoughSpaceException(); + } + + if ($size !== null) { + if ($size < $free) { + return parent::writeStream($path, $stream, $size); + } else { + throw new NotEnoughSpaceException(); + } + } else { + // force fallback through `fopen` to handle the quota + try { + return parent::writeStreamFallback($path, $stream); + } catch (GenericFileException) { + throw new NotEnoughSpaceException(); + } + } + } } diff --git a/lib/private/Files/Storage/Wrapper/Wrapper.php b/lib/private/Files/Storage/Wrapper/Wrapper.php index d5454050fcb09..d398dae2b6541 100644 --- a/lib/private/Files/Storage/Wrapper/Wrapper.php +++ b/lib/private/Files/Storage/Wrapper/Wrapper.php @@ -329,14 +329,21 @@ public function writeStream(string $path, $stream, ?int $size = null): int { /** @var IWriteStreamStorage $storage */ return $storage->writeStream($path, $stream, $size); } else { - $target = $this->fopen($path, 'w'); - $count = Files::streamCopy($stream, $target); - fclose($stream); - fclose($target); - return $count; + return $this->writeStreamFallback($path, $stream); } } + /** + * @param resource $stream + */ + protected function writeStreamFallback(string $path, $stream): int { + $target = $this->fopen($path, 'w'); + $count = Files::streamCopy($stream, $target); + fclose($stream); + fclose($target); + return $count; + } + public function getDirectoryContent(string $directory): \Traversable { return $this->getWrapperStorage()->getDirectoryContent($directory); } diff --git a/tests/lib/Files/Storage/Wrapper/QuotaTest.php b/tests/lib/Files/Storage/Wrapper/QuotaTest.php index e3a4e6dcb5f3f..cf939d0eb1064 100644 --- a/tests/lib/Files/Storage/Wrapper/QuotaTest.php +++ b/tests/lib/Files/Storage/Wrapper/QuotaTest.php @@ -235,4 +235,25 @@ public function testNoFopenQuotaZero(): void { $fh = $instance->fopen('files/test.txt', 'w'); $this->assertFalse($fh); } + + public function testNoWriteStreamQuota(): void { + $instance = $this->getLimitedStorage(5.0); + $stream = fopen('php://temp', 'w+'); + fwrite($stream, 'foo'); + rewind($stream); + $instance->writeStream('files/test.txt', $stream); + + $stream = fopen('php://temp', 'w+'); + fwrite($stream, 'foobar'); + rewind($stream); + $this->expectException(Files\NotEnoughSpaceException::class); + $instance->writeStream('files/test.txt', $stream); + } + + public function testNoWriteStreamQuotaZero(): void { + $instance = $this->getLimitedStorage(0.0); + $stream = fopen('php://temp', 'w+'); + $this->expectException(Files\NotEnoughSpaceException::class); + $instance->writeStream('files/test.txt', $stream); + } } From d7b2032e98288d25cbc5a06b2b32e92096f5683c Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 29 Apr 2026 19:15:26 +0200 Subject: [PATCH 3/5] fix: translate NotEnoughSpaceException to dav exception Signed-off-by: Robin Appelman --- apps/dav/lib/Connector/Sabre/File.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index 2f844b6519ce8..3c564aa3e31eb 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -633,6 +633,9 @@ private function convertToSabreException(\Exception $e) { if ($e instanceof NotFoundException) { throw new NotFound($this->l10n->t('File not found: %1$s', [$e->getMessage()]), 0, $e); } + if ($e instanceof Files\NotEnoughSpaceException) { + throw new EntityTooLarge($this->l10n->t('Insufficient space'), 0, $e); + } throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e); } From fa9db2c6320c45533818394bd1939260c3235866 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Wed, 1 Jul 2026 22:08:07 +0200 Subject: [PATCH 4/5] fix(files): allow quota write streams with unknown or unlimited space Assisted-by: Codex:GPT-5 Signed-off-by: veryCrunchy --- lib/private/Files/Storage/Wrapper/Quota.php | 2 +- tests/lib/Files/Storage/Wrapper/QuotaTest.php | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/private/Files/Storage/Wrapper/Quota.php b/lib/private/Files/Storage/Wrapper/Quota.php index 6b9b645623aa3..2dd3e85eeba9c 100644 --- a/lib/private/Files/Storage/Wrapper/Quota.php +++ b/lib/private/Files/Storage/Wrapper/Quota.php @@ -221,7 +221,7 @@ public function writeStream(string $path, $stream, ?int $size = null): int { } if ($size !== null) { - if ($size < $free) { + if ($free < 0 || $size < $free) { return parent::writeStream($path, $stream, $size); } else { throw new NotEnoughSpaceException(); diff --git a/tests/lib/Files/Storage/Wrapper/QuotaTest.php b/tests/lib/Files/Storage/Wrapper/QuotaTest.php index cf939d0eb1064..1ce8da80c073f 100644 --- a/tests/lib/Files/Storage/Wrapper/QuotaTest.php +++ b/tests/lib/Files/Storage/Wrapper/QuotaTest.php @@ -250,6 +250,26 @@ public function testNoWriteStreamQuota(): void { $instance->writeStream('files/test.txt', $stream); } + public function testWriteStreamAllowsUploadPathWithUnlimitedFreeSpace(): void { + $storage = $this->getMockBuilder(Local::class) + ->onlyMethods(['free_space']) + ->setConstructorArgs([['datadir' => $this->tmpDir]]) + ->getMock(); + $storage->expects($this->any()) + ->method('free_space') + ->willReturn(Files\FileInfo::SPACE_UNLIMITED); + $storage->mkdir('uploads'); + + $instance = new Quota(['storage' => $storage, 'quota' => 5.0]); + + $stream = fopen('php://temp', 'w+'); + fwrite($stream, 'foobar'); + rewind($stream); + + $this->assertEquals(6, $instance->writeStream('uploads/chunk', $stream, 6)); + $this->assertEquals('foobar', $instance->file_get_contents('uploads/chunk')); + } + public function testNoWriteStreamQuotaZero(): void { $instance = $this->getLimitedStorage(0.0); $stream = fopen('php://temp', 'w+'); From 76d25b35b2c4dd514053119c92eb73a333a4360f Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 16 Sep 2026 19:21:01 +0200 Subject: [PATCH 5/5] fix: properly throw error when write stream gets truncated Signed-off-by: Robin Appelman --- lib/private/Files/Storage/Wrapper/Wrapper.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/private/Files/Storage/Wrapper/Wrapper.php b/lib/private/Files/Storage/Wrapper/Wrapper.php index d398dae2b6541..d0c2ac6f8b840 100644 --- a/lib/private/Files/Storage/Wrapper/Wrapper.php +++ b/lib/private/Files/Storage/Wrapper/Wrapper.php @@ -9,12 +9,12 @@ use OC\Files\Storage\FailedStorage; use OC\Files\Storage\Storage; -use OCP\Files; use OCP\Files\Cache\ICache; use OCP\Files\Cache\IPropagator; use OCP\Files\Cache\IScanner; use OCP\Files\Cache\IUpdater; use OCP\Files\Cache\IWatcher; +use OCP\Files\GenericFileException; use OCP\Files\Storage\ILockingStorage; use OCP\Files\Storage\IStorage; use OCP\Files\Storage\IWriteStreamStorage; @@ -338,9 +338,17 @@ public function writeStream(string $path, $stream, ?int $size = null): int { */ protected function writeStreamFallback(string $path, $stream): int { $target = $this->fopen($path, 'w'); - $count = Files::streamCopy($stream, $target); + if ($target === false) { + throw new GenericFileException('Failed to open ' . $path); + } + + $count = stream_copy_to_stream($stream, $target); fclose($stream); fclose($target); + if ($count === false) { + throw new GenericFileException('Failed to copy stream.'); + } + return $count; }