fix: validate written size for s3 multipart uploads

Signed-off-by: Robin Appelman <robin@icewind.nl>
pull/54297/head
Robin Appelman 2025-07-28 19:55:20 +07:00
parent e483387189
commit 902cb3dbb9
No known key found for this signature in database
GPG Key ID: 42B69D8A64526EFB
3 changed files with 36 additions and 9 deletions

@ -201,6 +201,9 @@ class File extends Node implements IFile {
}
}
$lengthHeader = $this->request->getHeader('content-length');
$expected = $lengthHeader !== '' ? (int)$lengthHeader : null;
if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) {
$isEOF = false;
$wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF): void {
@ -212,7 +215,7 @@ class File extends Node implements IFile {
$count = -1;
try {
/** @var IWriteStreamStorage $partStorage */
$count = $partStorage->writeStream($internalPartPath, $wrappedData);
$count = $partStorage->writeStream($internalPartPath, $wrappedData, $expected);
} catch (GenericFileException $e) {
$logger = Server::get(LoggerInterface::class);
$logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']);
@ -232,10 +235,7 @@ class File extends Node implements IFile {
[$count, $result] = \OC_Helper::streamCopy($data, $target);
fclose($target);
}
$lengthHeader = $this->request->getHeader('content-length');
$expected = $lengthHeader !== '' ? (int)$lengthHeader : -1;
if ($result === false && $expected >= 0) {
if ($result === false && $expected !== null) {
throw new Exception(
$this->l10n->t(
'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)',
@ -250,7 +250,7 @@ class File extends Node implements IFile {
// if content length is sent by client:
// double check if the file was fully received
// compare expected and actual size
if ($expected >= 0
if ($expected !== null
&& $expected !== $count
&& $this->request->getMethod() === 'PUT'
) {

@ -482,6 +482,9 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil
$metadata = [
'mimetype' => $mimetype,
];
if ($size) {
$metadata['size'] = $size;
}
$stat['mimetype'] = $mimetype;
$stat['etag'] = $this->getETag($path);

@ -6,6 +6,8 @@
*/
namespace OC\Files\ObjectStore;
use Aws\Command;
use Aws\Exception\MultipartUploadException;
use Aws\S3\Exception\S3MultipartUploadException;
use Aws\S3\MultipartCopy;
use Aws\S3\MultipartUploader;
@ -87,14 +89,20 @@ trait S3ObjectTrait {
* @throws \Exception when something goes wrong, message will be logged
*/
protected function writeSingle(string $urn, StreamInterface $stream, array $metaData): void {
$this->getConnection()->putObject([
$args = [
'Bucket' => $this->bucket,
'Key' => $urn,
'Body' => $stream,
'ACL' => 'private',
'ContentType' => $metaData['mimetype'] ?? null,
'StorageClass' => $this->storageClass,
] + $this->getSSECParameters());
] + $this->getSSECParameters();
if ($size = $stream->getSize()) {
$args['ContentLength'] = $size;
}
$this->getConnection()->putObject($args);
}
@ -112,6 +120,8 @@ trait S3ObjectTrait {
$concurrency = $this->concurrency;
$exception = null;
$state = null;
$size = $stream->getSize();
$totalWritten = 0;
// retry multipart upload once with concurrency at half on failure
while (!$uploaded && $attempts <= 1) {
@ -125,6 +135,15 @@ trait S3ObjectTrait {
'ContentType' => $metaData['mimetype'] ?? null,
'StorageClass' => $this->storageClass,
] + $this->getSSECParameters(),
'before_upload' => function (Command $command) use (&$totalWritten) {
$totalWritten += $command['ContentLength'];
},
'before_complete' => function ($_command) use (&$totalWritten, $size, &$uploader, &$attempts) {
if ($size !== null && $totalWritten != $size) {
$e = new \Exception('Incomplete multi part upload, expected ' . $size . ' bytes, wrote ' . $totalWritten);
throw new MultipartUploadException($uploader->getState(), $e);
}
},
]);
try {
@ -141,6 +160,9 @@ trait S3ObjectTrait {
if ($stream->isSeekable()) {
$stream->rewind();
}
} catch (MultipartUploadException $e) {
$exception = $e;
break;
}
}
@ -166,7 +188,9 @@ trait S3ObjectTrait {
public function writeObjectWithMetaData(string $urn, $stream, array $metaData): void {
$canSeek = fseek($stream, 0, SEEK_CUR) === 0;
$psrStream = Utils::streamFor($stream);
$psrStream = Utils::streamFor($stream, [
'size' => $metaData['size'] ?? null,
]);
$size = $psrStream->getSize();