* @author Robin Appelman * * @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 . * */ 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 */ class SeekableHttpStream implements File { private const PROTOCOL = 'httpseek'; private static $registered = false; /** * Registers the stream wrapper using the `httpseek://` url scheme * $return void */ private static function registerIfNeeded() { if (!self::$registered) { stream_wrapper_register( self::PROTOCOL, self::class ); self::$registered = true; } } /** * Open a readonly-seekable http stream * * The provided callback will be called with byte range and should return an http stream for the requested range * * @param callable $callback * @return false|resource */ public static function open(callable $callback) { $context = stream_context_create([ SeekableHttpStream::PROTOCOL => [ 'callback' => $callback ], ]); SeekableHttpStream::registerIfNeeded(); return fopen(SeekableHttpStream::PROTOCOL . '://', 'r', false, $context); } /** @var resource */ public $context; /** @var callable */ private $openCallback; /** @var resource */ private $current; /** @var int */ private $offset = 0; /** @var int */ private $length = 0; private function reconnect(int $start) { $range = $start . '-'; if ($this->current != null) { fclose($this->current); } $this->current = ($this->openCallback)($range); if ($this->current === false) { return false; } $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) { return false; } $contentRange = $rangeHeaders[0]; $content = trim(explode(':', $contentRange)[1]); $range = trim(explode(' ', $content)[1]); $begin = intval(explode('-', $range)[0]); $length = intval(explode('/', $range)[1]); if ($begin !== $start) { return false; } $this->offset = $begin; $this->length = $length; return true; } public function stream_open($path, $mode, $options, &$opened_path) { $options = stream_context_get_options($this->context)[self::PROTOCOL]; $this->openCallback = $options['callback']; return $this->reconnect(0); } public function stream_read($count) { if (!$this->current) { return false; } $ret = fread($this->current, $count); $this->offset += strlen($ret); return $ret; } public function stream_seek($offset, $whence = SEEK_SET) { switch ($whence) { case SEEK_SET: if ($offset === $this->offset) { return true; } return $this->reconnect($offset); case SEEK_CUR: if ($offset === 0) { return true; } return $this->reconnect($this->offset + $offset); case SEEK_END: if ($this->length === 0) { return false; } elseif ($this->length + $offset === $this->offset) { return true; } return $this->reconnect($this->length + $offset); } return false; } public function stream_tell() { return $this->offset; } public function stream_stat() { if (is_resource($this->current)) { return fstat($this->current); } else { return false; } } public function stream_eof() { if (is_resource($this->current)) { return feof($this->current); } else { return true; } } public function stream_close() { if (is_resource($this->current)) { fclose($this->current); } } public function stream_write($data) { return false; } public function stream_set_option($option, $arg1, $arg2) { return false; } public function stream_truncate($size) { return false; } public function stream_lock($operation) { return false; } public function stream_flush() { return; //noop because readonly stream } }