AssemblyStream.php 6.0 KB

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