aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Files/Stream/SeekableHttpStream.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Files/Stream/SeekableHttpStream.php')
-rw-r--r--lib/private/Files/Stream/SeekableHttpStream.php130
1 files changed, 88 insertions, 42 deletions
diff --git a/lib/private/Files/Stream/SeekableHttpStream.php b/lib/private/Files/Stream/SeekableHttpStream.php
index af797c7720d..6ce0a880e8d 100644
--- a/lib/private/Files/Stream/SeekableHttpStream.php
+++ b/lib/private/Files/Stream/SeekableHttpStream.php
@@ -1,29 +1,13 @@
<?php
+
/**
- * @copyright Copyright (c) 2020, Lukas Stabe (lukas@stabe.de)
- *
- * @author Lukas Stabe <lukas@stabe.de>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Files\Stream;
use Icewind\Streams\File;
+use Icewind\Streams\Wrapper;
/**
* A stream wrapper that uses http range requests to provide a seekable stream for http reading
@@ -31,7 +15,7 @@ use Icewind\Streams\File;
class SeekableHttpStream implements File {
private const PROTOCOL = 'httpseek';
- private static $registered = false;
+ private static bool $registered = false;
/**
* Registers the stream wrapper using the `httpseek://` url scheme
@@ -72,30 +56,49 @@ class SeekableHttpStream implements File {
/** @var callable */
private $openCallback;
- /** @var resource */
+ /** @var ?resource|closed-resource */
private $current;
- /** @var int */
- private $offset = 0;
- /** @var int */
- private $length = 0;
-
- private function reconnect(int $start) {
+ /** @var int $offset offset of the current chunk */
+ private int $offset = 0;
+ /** @var int $length length of the current chunk */
+ private int $length = 0;
+ /** @var int $totalSize size of the full stream */
+ private int $totalSize = 0;
+ private bool $needReconnect = false;
+
+ private function reconnect(int $start): bool {
+ $this->needReconnect = false;
$range = $start . '-';
- if ($this->current != null) {
+ if ($this->hasOpenStream()) {
fclose($this->current);
}
- $this->current = ($this->openCallback)($range);
+ $stream = ($this->openCallback)($range);
- if ($this->current === false) {
+ if ($stream === false) {
+ $this->current = null;
return false;
}
+ $this->current = $stream;
$responseHead = stream_get_meta_data($this->current)['wrapper_data'];
+
+ while ($responseHead instanceof Wrapper) {
+ $wrapperOptions = stream_context_get_options($responseHead->context);
+ foreach ($wrapperOptions as $options) {
+ if (isset($options['source']) && is_resource($options['source'])) {
+ $responseHead = stream_get_meta_data($options['source'])['wrapper_data'];
+ continue 2;
+ }
+ }
+ throw new \Exception('Failed to get source stream from stream wrapper of ' . get_class($responseHead));
+ }
+
$rangeHeaders = array_values(array_filter($responseHead, function ($v) {
return preg_match('#^content-range:#i', $v) === 1;
}));
if (!$rangeHeaders) {
+ $this->current = null;
return false;
}
$contentRange = $rangeHeaders[0];
@@ -106,15 +109,41 @@ class SeekableHttpStream implements File {
$length = intval(explode('/', $range)[1]);
if ($begin !== $start) {
+ $this->current = null;
return false;
}
$this->offset = $begin;
$this->length = $length;
+ if ($start === 0) {
+ $this->totalSize = $length;
+ }
return true;
}
+ /**
+ * @return ?resource
+ */
+ private function getCurrent() {
+ if ($this->needReconnect) {
+ $this->reconnect($this->offset);
+ }
+ if (is_resource($this->current)) {
+ return $this->current;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @return bool
+ * @psalm-assert-if-true resource $this->current
+ */
+ private function hasOpenStream(): bool {
+ return is_resource($this->current);
+ }
+
public function stream_open($path, $mode, $options, &$opened_path) {
$options = stream_context_get_options($this->context)[self::PROTOCOL];
$this->openCallback = $options['callback'];
@@ -123,10 +152,10 @@ class SeekableHttpStream implements File {
}
public function stream_read($count) {
- if (!$this->current) {
+ if (!$this->getCurrent()) {
return false;
}
- $ret = fread($this->current, $count);
+ $ret = fread($this->getCurrent(), $count);
$this->offset += strlen($ret);
return $ret;
}
@@ -136,22 +165,34 @@ class SeekableHttpStream implements File {
case SEEK_SET:
if ($offset === $this->offset) {
return true;
+ } else {
+ $this->offset = $offset;
}
- return $this->reconnect($offset);
+ break;
case SEEK_CUR:
if ($offset === 0) {
return true;
+ } else {
+ $this->offset += $offset;
}
- return $this->reconnect($this->offset + $offset);
+ break;
case SEEK_END:
if ($this->length === 0) {
return false;
} elseif ($this->length + $offset === $this->offset) {
return true;
+ } else {
+ $this->offset = $this->length + $offset;
}
- return $this->reconnect($this->length + $offset);
+ break;
}
- return false;
+
+ if ($this->hasOpenStream()) {
+ fclose($this->current);
+ }
+ $this->current = null;
+ $this->needReconnect = true;
+ return true;
}
public function stream_tell() {
@@ -159,25 +200,30 @@ class SeekableHttpStream implements File {
}
public function stream_stat() {
- if (is_resource($this->current)) {
- return fstat($this->current);
+ if ($this->getCurrent()) {
+ $stat = fstat($this->getCurrent());
+ if ($stat) {
+ $stat['size'] = $this->totalSize;
+ }
+ return $stat;
} else {
return false;
}
}
public function stream_eof() {
- if (is_resource($this->current)) {
- return feof($this->current);
+ if ($this->getCurrent()) {
+ return feof($this->getCurrent());
} else {
return true;
}
}
public function stream_close() {
- if (is_resource($this->current)) {
+ if ($this->hasOpenStream()) {
fclose($this->current);
}
+ $this->current = null;
}
public function stream_write($data) {