Skip to content

Commit 8a12a79

Browse files
committed
fix(s3): improve error on move/copy operations when bucket quota exceeded
Fixes: #58801 Signed-off-by: Jonas <jonas@freesources.org>
1 parent d353459 commit 8a12a79

5 files changed

Lines changed: 65 additions & 6 deletions

File tree

apps/dav/lib/Connector/Sabre/Directory.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
use OC\Files\Utils\PathHelper;
1111
use OC\Files\View;
1212
use OCA\DAV\AppInfo\Application;
13+
use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge;
1314
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
1415
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
1516
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
1617
use OCA\DAV\Storage\PublicShareWrapper;
1718
use OCP\App\IAppManager;
1819
use OCP\Constants;
20+
use OCP\Files\EntityTooLargeException;
1921
use OCP\Files\FileInfo;
2022
use OCP\Files\Folder;
2123
use OCP\Files\ForbiddenException;
@@ -447,6 +449,8 @@ public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
447449
if (!$renameOkay) {
448450
throw new \Sabre\DAV\Exception\Forbidden('');
449451
}
452+
} catch (EntityTooLargeException $e) {
453+
throw new EntityTooLarge($e->getMessage());
450454
} catch (StorageNotAvailableException $e) {
451455
throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
452456
} catch (ForbiddenException $ex) {
@@ -482,6 +486,8 @@ public function copyInto($targetName, $sourcePath, INode $sourceNode) {
482486
}
483487

484488
return true;
489+
} catch (EntityTooLargeException $e) {
490+
throw new EntityTooLarge($e->getMessage());
485491
} catch (StorageNotAvailableException $e) {
486492
throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
487493
} catch (ForbiddenException $ex) {

apps/files/src/actions/moveOrCopyAction.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ export async function* handleCopyMoveNodesTo(nodes: INode[], destination: IFolde
192192
logger.debug(`Error while trying to ${method === MoveCopyAction.COPY ? 'copy' : 'move'} node`, { node, error })
193193
if (error.response?.status === 412) {
194194
throw new HintException(t('files', 'A file or folder with that name already exists in this folder'))
195+
} else if (error.response?.status === 413) {
196+
throw new HintException(t('files', 'Insufficient storage, quota exceeded'))
195197
} else if (error.response?.status === 423) {
196198
throw new HintException(t('files', 'The files are locked'))
197199
} else if (error.response?.status === 404) {

lib/private/Files/ObjectStore/ObjectStoreStorage.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use OCP\Files\Cache\ICache;
2222
use OCP\Files\Cache\ICacheEntry;
2323
use OCP\Files\Cache\IScanner;
24+
use OCP\Files\EntityTooLargeException;
2425
use OCP\Files\FileInfo;
2526
use OCP\Files\GenericFileException;
2627
use OCP\Files\IMimeTypeDetector;
@@ -529,6 +530,11 @@ public function writeStream(string $path, $stream, ?int $size = null): int {
529530
}
530531

531532
$stat['size'] = $totalWritten;
533+
} catch (EntityTooLargeException $ex) {
534+
if (!$exists) {
535+
$this->getCache()->remove($uploadPath);
536+
}
537+
throw $ex;
532538
} catch (\Exception $ex) {
533539
if (!$exists) {
534540
/*

lib/private/Files/ObjectStore/S3ObjectTrait.php

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use GuzzleHttp\Psr7\Utils;
1818
use OC\Files\Stream\SeekableHttpStream;
1919
use OCA\DAV\Connector\Sabre\Exception\BadGateway;
20+
use OCP\Files\EntityTooLargeException;
2021
use Psr\Http\Message\StreamInterface;
2122

2223
trait S3ObjectTrait {
@@ -94,6 +95,24 @@ private function buildS3Metadata(array $metadata): array {
9495
return $result;
9596
}
9697

98+
private function isStorageFullException(\Throwable $e) {
99+
while ($e !== null) {
100+
if ($e instanceof AwsException) {
101+
// MinIO: dedicated error code for storage-full
102+
if ($e->getAwsErrorCode() === 'XMinioStorageFull') {
103+
return true;
104+
}
105+
// RustFS: returns generic error code but with a recognisable message
106+
if (str_starts_with($e->getAwsErrorMessage() ?? '', 'Bucket quota exceeded.')) {
107+
return true;
108+
}
109+
}
110+
$e = $e->getPrevious();
111+
}
112+
113+
return false;
114+
}
115+
97116
/**
98117
* Single object put helper
99118
*
@@ -121,7 +140,14 @@ protected function writeSingle(string $urn, StreamInterface $stream, array $meta
121140
$args['ContentLength'] = $size;
122141
}
123142

124-
$this->getConnection()->putObject($args);
143+
try {
144+
$this->getConnection()->putObject($args);
145+
} catch (AwsException $e) {
146+
if ($this->isStorageFullException($e)) {
147+
throw new EntityTooLargeException('Quota exceeded on S3 storage', 0, $e);
148+
}
149+
throw $e;
150+
}
125151
}
126152

127153

@@ -198,6 +224,10 @@ protected function writeMultiPart(string $urn, StreamInterface $stream, array $m
198224
$this->getConnection()->abortMultipartUpload($uploadInfo);
199225
}
200226

227+
if ($this->isStorageFullException($exception)) {
228+
throw new EntityTooLargeException('Quota exceeded on S3 storage', 0, $exception);
229+
}
230+
201231
throw new BadGateway('Error while uploading to S3 bucket', 0, $exception);
202232
}
203233
}
@@ -290,12 +320,24 @@ public function copyObject($from, $to, array $options = []) {
290320
'params' => $this->getServerSideEncryptionParameters() + $this->getServerSideEncryptionParameters(true),
291321
'source_metadata' => $sourceMetadata
292322
], $options));
293-
$copy->copy();
323+
try {
324+
$copy->copy();
325+
} catch (\Throwable $e) {
326+
if ($this->isStorageFullException($e)) {
327+
throw new EntityTooLargeException('Quota exceeded on S3 storage', 0, $e);
328+
}
329+
}
294330
} else {
295-
$this->getConnection()->copy($this->getBucket(), $from, $this->getBucket(), $to, 'private', array_merge([
296-
'params' => $this->getServerSideEncryptionParameters() + $this->getServerSideEncryptionParameters(true),
297-
'mup_threshold' => PHP_INT_MAX,
298-
], $options));
331+
try {
332+
$this->getConnection()->copy($this->getBucket(), $from, $this->getBucket(), $to, 'private', array_merge([
333+
'params' => $this->getServerSideEncryptionParameters() + $this->getServerSideEncryptionParameters(true),
334+
'mup_threshold' => PHP_INT_MAX,
335+
], $options));
336+
} catch (AwsException $e) {
337+
if ($this->isStorageFullException($e)) {
338+
throw new EntityTooLargeException('Quota exceeded on S3 storage', 0, $e);
339+
}
340+
}
299341
}
300342
}
301343

lib/private/Files/Storage/Common.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use OCP\Files\Cache\IScanner;
2727
use OCP\Files\Cache\IUpdater;
2828
use OCP\Files\Cache\IWatcher;
29+
use OCP\Files\EntityTooLargeException;
2930
use OCP\Files\FileInfo;
3031
use OCP\Files\ForbiddenException;
3132
use OCP\Files\GenericFileException;
@@ -530,6 +531,8 @@ public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalP
530531
try {
531532
$this->writeStream($targetInternalPath, $source);
532533
$result = true;
534+
} catch (EntityTooLargeException $e) {
535+
throw $e;
533536
} catch (\Exception $e) {
534537
Server::get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]);
535538
}

0 commit comments

Comments
 (0)