From a043a617d3301e99af48b1a1d3ee6c8dad341fa3 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 29 Apr 2026 19:13:09 +0200 Subject: [PATCH 1/3] fix: block writing empty files with 0 quota Signed-off-by: Robin Appelman --- lib/private/Files/Storage/Wrapper/Quota.php | 18 ++++++++++-------- tests/lib/Files/Storage/Wrapper/QuotaTest.php | 6 ++++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/private/Files/Storage/Wrapper/Quota.php b/lib/private/Files/Storage/Wrapper/Quota.php index 35a265f8c8e72..d721217d7ff96 100644 --- a/lib/private/Files/Storage/Wrapper/Quota.php +++ b/lib/private/Files/Storage/Wrapper/Quota.php @@ -121,14 +121,16 @@ public function fopen(string $path, string $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 2878fe6ca925f..27aeae5dd7fa0 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 1149b088792dbc63f9a071490edef895f3b2267c Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 29 Apr 2026 19:14:57 +0200 Subject: [PATCH 2/3] fix: apply quota with writeStream Signed-off-by: Robin Appelman --- lib/private/Files/Storage/Wrapper/Quota.php | 29 +++++++++++++++++++ tests/lib/Files/Storage/Wrapper/QuotaTest.php | 21 ++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/lib/private/Files/Storage/Wrapper/Quota.php b/lib/private/Files/Storage/Wrapper/Quota.php index d721217d7ff96..827048f4fe479 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 { @@ -207,4 +209,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/tests/lib/Files/Storage/Wrapper/QuotaTest.php b/tests/lib/Files/Storage/Wrapper/QuotaTest.php index 27aeae5dd7fa0..9033e41edbe39 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 69a04c541d11fc5677d9efe3cb61eed0c62dd893 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 29 Apr 2026 19:15:26 +0200 Subject: [PATCH 3/3] fix: translate NotEnoughSpaceException to dav exception fix: translate NotEnoughSpaceException to dav exception Signed-off-by: Robin Appelman [skip ci] --- 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 b1a0a93968dd5..453502c756589 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -604,6 +604,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); }