Skip to content

Commit 119727d

Browse files
Merge pull request #52759 from nextcloud/backport/49352/stable30
2 parents 126b793 + 87a4bff commit 119727d

File tree

4 files changed

+63
-21
lines changed

4 files changed

+63
-21
lines changed

apps/dav/lib/Upload/AssemblyStream.php

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ public function stream_seek($offset, $whence = SEEK_SET) {
7575
$offset = $this->size + $offset;
7676
}
7777

78+
if ($offset === $this->pos) {
79+
return true;
80+
}
81+
7882
if ($offset > $this->size) {
7983
return false;
8084
}
@@ -95,7 +99,7 @@ public function stream_seek($offset, $whence = SEEK_SET) {
9599

96100
$stream = $this->getStream($this->nodes[$nodeIndex]);
97101
$nodeOffset = $offset - $nodeStart;
98-
if (fseek($stream, $nodeOffset) === -1) {
102+
if ($nodeOffset > 0 && fseek($stream, $nodeOffset) === -1) {
99103
return false;
100104
}
101105
$this->currentNode = $nodeIndex;
@@ -126,9 +130,14 @@ public function stream_read($count) {
126130
}
127131
}
128132

129-
do {
133+
$collectedData = '';
134+
// read data until we either got all the data requested or there is no more stream left
135+
while ($count > 0 && !is_null($this->currentStream)) {
130136
$data = fread($this->currentStream, $count);
131137
$read = strlen($data);
138+
139+
$count -= $read;
140+
$collectedData .= $data;
132141
$this->currentNodeRead += $read;
133142

134143
if (feof($this->currentStream)) {
@@ -145,14 +154,11 @@ public function stream_read($count) {
145154
$this->currentStream = null;
146155
}
147156
}
148-
// if no data read, try again with the next node because
149-
// returning empty data can make the caller think there is no more
150-
// data left to read
151-
} while ($read === 0 && !is_null($this->currentStream));
157+
}
152158

153159
// update position
154-
$this->pos += $read;
155-
return $data;
160+
$this->pos += strlen($collectedData);
161+
return $collectedData;
156162
}
157163

158164
/**

apps/dav/tests/unit/Upload/AssemblyStreamTest.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ public function testGetContents($expected, $nodes): void {
2424
/**
2525
* @dataProvider providesNodes()
2626
*/
27-
public function testGetContentsFread($expected, $nodes): void {
27+
public function testGetContentsFread($expected, $nodes, $chunkLength = 3): void {
2828
$stream = \OCA\DAV\Upload\AssemblyStream::wrap($nodes);
2929

3030
$content = '';
3131
while (!feof($stream)) {
32-
$content .= fread($stream, 3);
32+
$chunk = fread($stream, $chunkLength);
33+
$content .= $chunk;
34+
if ($chunkLength !== 3) {
35+
$this->assertEquals($chunkLength, strlen($chunk));
36+
}
3337
}
3438

3539
$this->assertEquals($expected, $content);
@@ -102,7 +106,19 @@ public function providesNodes() {
102106
]],
103107
'a ton of nodes' => [
104108
$tonofdata, $tonofnodes
105-
]
109+
],
110+
'one read over multiple nodes' => [
111+
'1234567890', [
112+
$this->buildNode('0', '1234'),
113+
$this->buildNode('1', '5678'),
114+
$this->buildNode('2', '90'),
115+
], 10],
116+
'two reads over multiple nodes' => [
117+
'1234567890', [
118+
$this->buildNode('0', '1234'),
119+
$this->buildNode('1', '5678'),
120+
$this->buildNode('2', '90'),
121+
], 5],
106122
];
107123
}
108124

lib/private/Files/ObjectStore/ObjectStoreStorage.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,13 @@ public function file_put_contents($path, $data) {
474474
}
475475

476476
public function writeStream(string $path, $stream, ?int $size = null): int {
477+
if ($size === null) {
478+
$stats = fstat($stream);
479+
if (is_array($stats) && isset($stats['size'])) {
480+
$size = $stats['size'];
481+
}
482+
}
483+
477484
$stat = $this->stat($path);
478485
if (empty($stat)) {
479486
// create new file

lib/private/Files/ObjectStore/S3ObjectTrait.php

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,20 +140,33 @@ protected function writeMultiPart(string $urn, StreamInterface $stream, ?string
140140
* @since 7.0.0
141141
*/
142142
public function writeObject($urn, $stream, ?string $mimetype = null) {
143+
$canSeek = fseek($stream, 0, SEEK_CUR) === 0;
143144
$psrStream = Utils::streamFor($stream);
144145

145-
// ($psrStream->isSeekable() && $psrStream->getSize() !== null) evaluates to true for a On-Seekable stream
146-
// so the optimisation does not apply
147-
$buffer = new Psr7\Stream(fopen('php://memory', 'rwb+'));
148-
Utils::copyToStream($psrStream, $buffer, $this->putSizeLimit);
149-
$buffer->seek(0);
150-
if ($buffer->getSize() < $this->putSizeLimit) {
151-
// buffer is fully seekable, so use it directly for the small upload
152-
$this->writeSingle($urn, $buffer, $mimetype);
146+
147+
$size = $psrStream->getSize();
148+
if ($size === null || !$canSeek) {
149+
// The s3 single-part upload requires the size to be known for the stream.
150+
// So for input streams that don't have a known size, we need to copy (part of)
151+
// the input into a temporary stream so the size can be determined
152+
$buffer = new Psr7\Stream(fopen('php://temp', 'rw+'));
153+
Utils::copyToStream($psrStream, $buffer, $this->putSizeLimit);
154+
$buffer->seek(0);
155+
if ($buffer->getSize() < $this->putSizeLimit) {
156+
// buffer is fully seekable, so use it directly for the small upload
157+
$this->writeSingle($urn, $buffer, $mimetype);
158+
} else {
159+
$loadStream = new Psr7\AppendStream([$buffer, $psrStream]);
160+
$this->writeMultiPart($urn, $loadStream, $mimetype);
161+
}
153162
} else {
154-
$loadStream = new Psr7\AppendStream([$buffer, $psrStream]);
155-
$this->writeMultiPart($urn, $loadStream, $mimetype);
163+
if ($size < $this->putSizeLimit) {
164+
$this->writeSingle($urn, $psrStream, $mimetype);
165+
} else {
166+
$this->writeMultiPart($urn, $psrStream, $mimetype);
167+
}
156168
}
169+
$psrStream->close();
157170
}
158171

159172
/**

0 commit comments

Comments
 (0)