Merge pull request #54297 from nextcloud/backport/54125/stable31

This commit is contained in:
Benjamin Gaussorgues 2025-08-07 17:00:12 +02:00 committed by GitHub
commit 50fc614bcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 135 additions and 83 deletions

@ -1 +1 @@
Subproject commit 1145cdd2a4b998183f143e48ff2834d7865ed4e5
Subproject commit e3123f6fa0555a5e951d70f530097f8c94ad170c

View file

@ -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'
) {

View file

@ -9,6 +9,6 @@
},
"require": {
"icewind/smb": "3.7.0",
"icewind/streams": "0.7.7"
"icewind/streams": "0.7.8"
}
}

View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1b75bb2715ed2dae7d090ae40b9843a2",
"content-hash": "565bb9d1046efbd95b08a2b4df1a4efb",
"packages": [
{
"name": "icewind/smb",
@ -55,17 +55,11 @@
},
{
"name": "icewind/streams",
"version": "v0.7.7",
"version": "v0.7.8",
"source": {
"type": "git",
"url": "https://github.com/icewind1991/Streams.git",
"reference": "64200fd7cfcc7f550c3c695c48d8fd8bba97fecb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/icewind1991/Streams/zipball/64200fd7cfcc7f550c3c695c48d8fd8bba97fecb",
"reference": "64200fd7cfcc7f550c3c695c48d8fd8bba97fecb",
"shasum": ""
"url": "https://codeberg.org/icewind/streams",
"reference": "cb2bd3ed41b516efb97e06e8da35a12ef58ba48b"
},
"require": {
"php": ">=7.1"
@ -92,11 +86,7 @@
}
],
"description": "A set of generic stream wrappers",
"support": {
"issues": "https://github.com/icewind1991/Streams/issues",
"source": "https://github.com/icewind1991/Streams/tree/v0.7.7"
},
"time": "2023-03-16T14:52:25+00:00"
"time": "2024-12-05T14:36:22+00:00"
}
],
"packages-dev": [],

View file

@ -32,6 +32,11 @@ class InstalledVersions
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
@ -309,6 +314,12 @@ class InstalledVersions
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
@ -322,19 +333,27 @@ class InstalledVersions
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = strtr(__DIR__, '\\', '/');
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
@ -350,7 +369,7 @@ class InstalledVersions
}
}
if (self::$installed !== array()) {
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}

View file

@ -52,18 +52,12 @@
},
{
"name": "icewind/streams",
"version": "v0.7.7",
"version_normalized": "0.7.7.0",
"version": "v0.7.8",
"version_normalized": "0.7.8.0",
"source": {
"type": "git",
"url": "https://github.com/icewind1991/Streams.git",
"reference": "64200fd7cfcc7f550c3c695c48d8fd8bba97fecb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/icewind1991/Streams/zipball/64200fd7cfcc7f550c3c695c48d8fd8bba97fecb",
"reference": "64200fd7cfcc7f550c3c695c48d8fd8bba97fecb",
"shasum": ""
"url": "https://codeberg.org/icewind/streams",
"reference": "cb2bd3ed41b516efb97e06e8da35a12ef58ba48b"
},
"require": {
"php": ">=7.1"
@ -73,9 +67,9 @@
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^9"
},
"time": "2023-03-16T14:52:25+00:00",
"time": "2024-12-05T14:36:22+00:00",
"type": "library",
"installation-source": "dist",
"installation-source": "source",
"autoload": {
"psr-4": {
"Icewind\\Streams\\": "src/"
@ -92,10 +86,6 @@
}
],
"description": "A set of generic stream wrappers",
"support": {
"issues": "https://github.com/icewind1991/Streams/issues",
"source": "https://github.com/icewind1991/Streams/tree/v0.7.7"
},
"install-path": "../icewind/streams"
}
],

View file

@ -3,7 +3,7 @@
'name' => 'files_external/3rdparty',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '101c7575ae7684a572e53740c63635cd12685995',
'reference' => '05e64418a77134e55824fc8c1a91125805428f3f',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
@ -13,7 +13,7 @@
'files_external/3rdparty' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '101c7575ae7684a572e53740c63635cd12685995',
'reference' => '05e64418a77134e55824fc8c1a91125805428f3f',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
@ -29,9 +29,9 @@
'dev_requirement' => false,
),
'icewind/streams' => array(
'pretty_version' => 'v0.7.7',
'version' => '0.7.7.0',
'reference' => '64200fd7cfcc7f550c3c695c48d8fd8bba97fecb',
'pretty_version' => 'v0.7.8',
'version' => '0.7.8.0',
'reference' => 'cb2bd3ed41b516efb97e06e8da35a12ef58ba48b',
'type' => 'library',
'install_path' => __DIR__ . '/../icewind/streams',
'aliases' => array(),

View file

@ -60,6 +60,17 @@ class CountWrapper extends Wrapper {
return true;
}
public function stream_seek($offset, $whence = SEEK_SET) {
if ($whence === SEEK_SET) {
$this->readCount = $offset;
$this->writeCount = $offset;
} else if ($whence === SEEK_CUR) {
$this->readCount += $offset;
$this->writeCount += $offset;
}
return parent::stream_seek($offset, $whence);
}
public function dir_opendir($path, $options) {
return $this->open();
}

View file

@ -33,4 +33,13 @@ interface IObjectStoreMetaData {
* @since 32.0.0
*/
public function listObjects(string $prefix = ''): \Iterator;
/**
* @param string $urn the unified resource name used to identify the object
* @param resource $stream stream with the data to write
* @param ObjectMetaData $metaData the metadata to set for the object
* @throws \Exception when something goes wrong, message will be logged
* @since 32.0.0
*/
public function writeObjectWithMetaData(string $urn, $stream, array $metaData): void;
}

View file

@ -479,6 +479,12 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil
$mimetypeDetector = \OC::$server->getMimeTypeDetector();
$mimetype = $mimetypeDetector->detectPath($path);
$metadata = [
'mimetype' => $mimetype,
];
if ($size) {
$metadata['size'] = $size;
}
$stat['mimetype'] = $mimetype;
$stat['etag'] = $this->getETag($path);
@ -500,24 +506,27 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil
$urn = $this->getURN($fileId);
try {
//upload to object storage
if ($size === null) {
$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
$totalWritten = 0;
$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, $size, $exists, &$totalWritten) {
if (is_null($size) && !$exists) {
$this->getCache()->update($fileId, [
'size' => $writtenSize,
]);
$size = $writtenSize;
});
$this->objectStore->writeObject($urn, $countStream, $mimetype);
if (is_resource($countStream)) {
fclose($countStream);
}
$stat['size'] = $size;
$totalWritten = $writtenSize;
});
if ($this->objectStore instanceof IObjectStoreMetaData) {
$this->objectStore->writeObjectWithMetaData($urn, $countStream, $metadata);
} else {
$this->objectStore->writeObject($urn, $stream, $mimetype);
if (is_resource($stream)) {
fclose($stream);
}
$this->objectStore->writeObject($urn, $countStream, $metadata['mimetype']);
}
if (is_resource($countStream)) {
fclose($countStream);
}
$stat['size'] = $totalWritten;
} catch (\Exception $ex) {
if (!$exists) {
/*
@ -541,7 +550,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil
]
);
}
throw $ex; // make this bubble up
throw new GenericFileException('Error while writing stream to object store', 0, $ex);
}
if ($exists) {
@ -557,7 +566,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil
}
}
return $size;
return $totalWritten;
}
public function getObjectStore(): IObjectStore {

View file

@ -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;
@ -83,18 +85,24 @@ trait S3ObjectTrait {
*
* @param string $urn the unified resource name used to identify the object
* @param StreamInterface $stream stream with the data to write
* @param string|null $mimetype the mimetype to set for the remove object @since 22.0.0
* @param array $metaData the metadata to set for the object
* @throws \Exception when something goes wrong, message will be logged
*/
protected function writeSingle(string $urn, StreamInterface $stream, ?string $mimetype = null): void {
$this->getConnection()->putObject([
protected function writeSingle(string $urn, StreamInterface $stream, array $metaData): void {
$args = [
'Bucket' => $this->bucket,
'Key' => $urn,
'Body' => $stream,
'ACL' => 'private',
'ContentType' => $mimetype,
'ContentType' => $metaData['mimetype'] ?? null,
'StorageClass' => $this->storageClass,
] + $this->getSSECParameters());
] + $this->getSSECParameters();
if ($size = $stream->getSize()) {
$args['ContentLength'] = $size;
}
$this->getConnection()->putObject($args);
}
@ -103,15 +111,17 @@ trait S3ObjectTrait {
*
* @param string $urn the unified resource name used to identify the object
* @param StreamInterface $stream stream with the data to write
* @param string|null $mimetype the mimetype to set for the remove object
* @param array $metaData the metadata to set for the object
* @throws \Exception when something goes wrong, message will be logged
*/
protected function writeMultiPart(string $urn, StreamInterface $stream, ?string $mimetype = null): void {
protected function writeMultiPart(string $urn, StreamInterface $stream, array $metaData): void {
$attempts = 0;
$uploaded = false;
$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) {
@ -122,9 +132,18 @@ trait S3ObjectTrait {
'part_size' => $this->uploadPartSize,
'state' => $state,
'params' => [
'ContentType' => $mimetype,
'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;
}
}
@ -156,17 +178,19 @@ trait S3ObjectTrait {
}
}
/**
* @param string $urn the unified resource name used to identify the object
* @param resource $stream stream with the data to write
* @param string|null $mimetype the mimetype to set for the remove object @since 22.0.0
* @throws \Exception when something goes wrong, message will be logged
* @since 7.0.0
*/
public function writeObject($urn, $stream, ?string $mimetype = null) {
$metaData = [];
if ($mimetype) {
$metaData['mimetype'] = $mimetype;
}
$this->writeObjectWithMetaData($urn, $stream, $metaData);
}
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();
@ -179,16 +203,16 @@ trait S3ObjectTrait {
$buffer->seek(0);
if ($buffer->getSize() < $this->putSizeLimit) {
// buffer is fully seekable, so use it directly for the small upload
$this->writeSingle($urn, $buffer, $mimetype);
$this->writeSingle($urn, $buffer, $metaData);
} else {
$loadStream = new Psr7\AppendStream([$buffer, $psrStream]);
$this->writeMultiPart($urn, $loadStream, $mimetype);
$this->writeMultiPart($urn, $loadStream, $metaData);
}
} else {
if ($size < $this->putSizeLimit) {
$this->writeSingle($urn, $psrStream, $mimetype);
$this->writeSingle($urn, $psrStream, $metaData);
} else {
$this->writeMultiPart($urn, $psrStream, $mimetype);
$this->writeMultiPart($urn, $psrStream, $metaData);
}
}
$psrStream->close();