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.

AssemblyStream.php 6.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author J0WI <J0WI@users.noreply.github.com>
  7. * @author Lukas Reschke <lukas@statuscode.ch>
  8. * @author Markus Goetz <markus@woboq.com>
  9. * @author Robin Appelman <robin@icewind.nl>
  10. * @author Roeland Jago Douma <roeland@famdouma.nl>
  11. * @author Thomas Müller <thomas.mueller@tmit.eu>
  12. * @author Vincent Petry <vincent@nextcloud.com>
  13. *
  14. * @license AGPL-3.0
  15. *
  16. * This code is free software: you can redistribute it and/or modify
  17. * it under the terms of the GNU Affero General Public License, version 3,
  18. * as published by the Free Software Foundation.
  19. *
  20. * This program is distributed in the hope that it will be useful,
  21. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  22. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  23. * GNU Affero General Public License for more details.
  24. *
  25. * You should have received a copy of the GNU Affero General Public License, version 3,
  26. * along with this program. If not, see <http://www.gnu.org/licenses/>
  27. *
  28. */
  29. namespace OCA\DAV\Upload;
  30. use Sabre\DAV\IFile;
  31. /**
  32. * Class AssemblyStream
  33. *
  34. * The assembly stream is a virtual stream that wraps multiple chunks.
  35. * Reading from the stream transparently accessed the underlying chunks and
  36. * give a representation as if they were already merged together.
  37. *
  38. * @package OCA\DAV\Upload
  39. */
  40. class AssemblyStream implements \Icewind\Streams\File {
  41. /** @var resource */
  42. private $context;
  43. /** @var IFile[] */
  44. private $nodes;
  45. /** @var int */
  46. private $pos = 0;
  47. /** @var int */
  48. private $size = 0;
  49. /** @var resource */
  50. private $currentStream = null;
  51. /** @var int */
  52. private $currentNode = 0;
  53. /** @var int */
  54. private $currentNodeRead = 0;
  55. /**
  56. * @param string $path
  57. * @param string $mode
  58. * @param int $options
  59. * @param string &$opened_path
  60. * @return bool
  61. */
  62. public function stream_open($path, $mode, $options, &$opened_path) {
  63. $this->loadContext('assembly');
  64. $nodes = $this->nodes;
  65. // https://stackoverflow.com/a/10985500
  66. @usort($nodes, function (IFile $a, IFile $b) {
  67. return strnatcmp($a->getName(), $b->getName());
  68. });
  69. $this->nodes = array_values($nodes);
  70. $this->size = array_reduce($this->nodes, function ($size, IFile $file) {
  71. return $size + $file->getSize();
  72. }, 0);
  73. return true;
  74. }
  75. /**
  76. * @param int $offset
  77. * @param int $whence
  78. * @return bool
  79. */
  80. public function stream_seek($offset, $whence = SEEK_SET) {
  81. if ($whence === SEEK_CUR) {
  82. $offset = $this->stream_tell() + $offset;
  83. } elseif ($whence === SEEK_END) {
  84. $offset = $this->size + $offset;
  85. }
  86. if ($offset > $this->size) {
  87. return false;
  88. }
  89. $nodeIndex = 0;
  90. $nodeStart = 0;
  91. while (true) {
  92. if (!isset($this->nodes[$nodeIndex + 1])) {
  93. break;
  94. }
  95. $node = $this->nodes[$nodeIndex];
  96. if ($nodeStart + $node->getSize() > $offset) {
  97. break;
  98. }
  99. $nodeIndex++;
  100. $nodeStart += $node->getSize();
  101. }
  102. $stream = $this->getStream($this->nodes[$nodeIndex]);
  103. $nodeOffset = $offset - $nodeStart;
  104. if (fseek($stream, $nodeOffset) === -1) {
  105. return false;
  106. }
  107. $this->currentNode = $nodeIndex;
  108. $this->currentNodeRead = $nodeOffset;
  109. $this->currentStream = $stream;
  110. $this->pos = $offset;
  111. return true;
  112. }
  113. /**
  114. * @return int
  115. */
  116. public function stream_tell() {
  117. return $this->pos;
  118. }
  119. /**
  120. * @param int $count
  121. * @return string
  122. */
  123. public function stream_read($count) {
  124. if (is_null($this->currentStream)) {
  125. if ($this->currentNode < count($this->nodes)) {
  126. $this->currentStream = $this->getStream($this->nodes[$this->currentNode]);
  127. } else {
  128. return '';
  129. }
  130. }
  131. do {
  132. $data = fread($this->currentStream, $count);
  133. $read = strlen($data);
  134. $this->currentNodeRead += $read;
  135. if (feof($this->currentStream)) {
  136. fclose($this->currentStream);
  137. $currentNodeSize = $this->nodes[$this->currentNode]->getSize();
  138. if ($this->currentNodeRead < $currentNodeSize) {
  139. throw new \Exception('Stream from assembly node shorter than expected, got ' . $this->currentNodeRead . ' bytes, expected ' . $currentNodeSize);
  140. }
  141. $this->currentNode++;
  142. $this->currentNodeRead = 0;
  143. if ($this->currentNode < count($this->nodes)) {
  144. $this->currentStream = $this->getStream($this->nodes[$this->currentNode]);
  145. } else {
  146. $this->currentStream = null;
  147. }
  148. }
  149. // if no data read, try again with the next node because
  150. // returning empty data can make the caller think there is no more
  151. // data left to read
  152. } while ($read === 0 && !is_null($this->currentStream));
  153. // update position
  154. $this->pos += $read;
  155. return $data;
  156. }
  157. /**
  158. * @param string $data
  159. * @return int
  160. */
  161. public function stream_write($data) {
  162. return false;
  163. }
  164. /**
  165. * @param int $option
  166. * @param int $arg1
  167. * @param int $arg2
  168. * @return bool
  169. */
  170. public function stream_set_option($option, $arg1, $arg2) {
  171. return false;
  172. }
  173. /**
  174. * @param int $size
  175. * @return bool
  176. */
  177. public function stream_truncate($size) {
  178. return false;
  179. }
  180. /**
  181. * @return array
  182. */
  183. public function stream_stat() {
  184. return [
  185. 'size' => $this->size,
  186. ];
  187. }
  188. /**
  189. * @param int $operation
  190. * @return bool
  191. */
  192. public function stream_lock($operation) {
  193. return false;
  194. }
  195. /**
  196. * @return bool
  197. */
  198. public function stream_flush() {
  199. return false;
  200. }
  201. /**
  202. * @return bool
  203. */
  204. public function stream_eof() {
  205. return $this->pos >= $this->size || ($this->currentNode >= count($this->nodes) && $this->currentNode === null);
  206. }
  207. /**
  208. * @return bool
  209. */
  210. public function stream_close() {
  211. return true;
  212. }
  213. /**
  214. * Load the source from the stream context and return the context options
  215. *
  216. * @param string $name
  217. * @return array
  218. * @throws \BadMethodCallException
  219. */
  220. protected function loadContext($name) {
  221. $context = stream_context_get_options($this->context);
  222. if (isset($context[$name])) {
  223. $context = $context[$name];
  224. } else {
  225. throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
  226. }
  227. if (isset($context['nodes']) and is_array($context['nodes'])) {
  228. $this->nodes = $context['nodes'];
  229. } else {
  230. throw new \BadMethodCallException('Invalid context, nodes not set');
  231. }
  232. return $context;
  233. }
  234. /**
  235. * @param IFile[] $nodes
  236. * @return resource
  237. *
  238. * @throws \BadMethodCallException
  239. */
  240. public static function wrap(array $nodes) {
  241. $context = stream_context_create([
  242. 'assembly' => [
  243. 'nodes' => $nodes
  244. ]
  245. ]);
  246. stream_wrapper_register('assembly', self::class);
  247. try {
  248. $wrapped = fopen('assembly://', 'r', null, $context);
  249. } catch (\BadMethodCallException $e) {
  250. stream_wrapper_unregister('assembly');
  251. throw $e;
  252. }
  253. stream_wrapper_unregister('assembly');
  254. return $wrapped;
  255. }
  256. /**
  257. * @param IFile $node
  258. * @return resource
  259. */
  260. private function getStream(IFile $node) {
  261. $data = $node->get();
  262. if (is_resource($data)) {
  263. return $data;
  264. } else {
  265. $tmp = fopen('php://temp', 'w+');
  266. fwrite($tmp, $data);
  267. rewind($tmp);
  268. return $tmp;
  269. }
  270. }
  271. }