You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

194 lines
4.4 KiB

3 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2020, Lukas Stabe (lukas@stabe.de)
  4. *
  5. * @author Lukas Stabe <lukas@stabe.de>
  6. * @author Robin Appelman <robin@icewind.nl>
  7. *
  8. * @license GNU AGPL version 3 or any later version
  9. *
  10. * This program is free software: you can redistribute it and/or modify
  11. * it under the terms of the GNU Affero General Public License as
  12. * published by the Free Software Foundation, either version 3 of the
  13. * License, or (at your option) any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU Affero General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public License
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. *
  23. */
  24. namespace OC\Files\Stream;
  25. use Icewind\Streams\File;
  26. /**
  27. * A stream wrapper that uses http range requests to provide a seekable stream for http reading
  28. */
  29. class SeekableHttpStream implements File {
  30. private const PROTOCOL = 'httpseek';
  31. private static $registered = false;
  32. /**
  33. * Registers the stream wrapper using the `httpseek://` url scheme
  34. * $return void
  35. */
  36. private static function registerIfNeeded() {
  37. if (!self::$registered) {
  38. stream_wrapper_register(
  39. self::PROTOCOL,
  40. self::class
  41. );
  42. self::$registered = true;
  43. }
  44. }
  45. /**
  46. * Open a readonly-seekable http stream
  47. *
  48. * The provided callback will be called with byte range and should return an http stream for the requested range
  49. *
  50. * @param callable $callback
  51. * @return false|resource
  52. */
  53. public static function open(callable $callback) {
  54. $context = stream_context_create([
  55. SeekableHttpStream::PROTOCOL => [
  56. 'callback' => $callback
  57. ],
  58. ]);
  59. SeekableHttpStream::registerIfNeeded();
  60. return fopen(SeekableHttpStream::PROTOCOL . '://', 'r', false, $context);
  61. }
  62. /** @var resource */
  63. public $context;
  64. /** @var callable */
  65. private $openCallback;
  66. /** @var resource */
  67. private $current;
  68. /** @var int */
  69. private $offset = 0;
  70. private function reconnect(int $start) {
  71. $range = $start . '-';
  72. if ($this->current != null) {
  73. fclose($this->current);
  74. }
  75. $this->current = ($this->openCallback)($range);
  76. if ($this->current === false) {
  77. return false;
  78. }
  79. $responseHead = stream_get_meta_data($this->current)['wrapper_data'];
  80. $rangeHeaders = array_values(array_filter($responseHead, function ($v) {
  81. return preg_match('#^content-range:#i', $v) === 1;
  82. }));
  83. if (!$rangeHeaders) {
  84. return false;
  85. }
  86. $contentRange = $rangeHeaders[0];
  87. $content = trim(explode(':', $contentRange)[1]);
  88. $range = trim(explode(' ', $content)[1]);
  89. $begin = intval(explode('-', $range)[0]);
  90. if ($begin !== $start) {
  91. return false;
  92. }
  93. $this->offset = $begin;
  94. return true;
  95. }
  96. public function stream_open($path, $mode, $options, &$opened_path) {
  97. $options = stream_context_get_options($this->context)[self::PROTOCOL];
  98. $this->openCallback = $options['callback'];
  99. return $this->reconnect(0);
  100. }
  101. public function stream_read($count) {
  102. if (!$this->current) {
  103. return false;
  104. }
  105. $ret = fread($this->current, $count);
  106. $this->offset += strlen($ret);
  107. return $ret;
  108. }
  109. public function stream_seek($offset, $whence = SEEK_SET) {
  110. switch ($whence) {
  111. case SEEK_SET:
  112. if ($offset === $this->offset) {
  113. return true;
  114. }
  115. return $this->reconnect($offset);
  116. case SEEK_CUR:
  117. if ($offset === 0) {
  118. return true;
  119. }
  120. return $this->reconnect($this->offset + $offset);
  121. case SEEK_END:
  122. return false;
  123. }
  124. return false;
  125. }
  126. public function stream_tell() {
  127. return $this->offset;
  128. }
  129. public function stream_stat() {
  130. if (is_resource($this->current)) {
  131. return fstat($this->current);
  132. } else {
  133. return false;
  134. }
  135. }
  136. public function stream_eof() {
  137. if (is_resource($this->current)) {
  138. return feof($this->current);
  139. } else {
  140. return true;
  141. }
  142. }
  143. public function stream_close() {
  144. if (is_resource($this->current)) {
  145. fclose($this->current);
  146. }
  147. }
  148. public function stream_write($data) {
  149. return false;
  150. }
  151. public function stream_set_option($option, $arg1, $arg2) {
  152. return false;
  153. }
  154. public function stream_truncate($size) {
  155. return false;
  156. }
  157. public function stream_lock($operation) {
  158. return false;
  159. }
  160. public function stream_flush() {
  161. return; //noop because readonly stream
  162. }
  163. }