SliceUploading.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. <?php
  2. namespace app\common\services\qcloud;
  3. /**
  4. * Uploading file to cos slice by slice.
  5. */
  6. class SliceUploading {
  7. // default task number for concurrently uploading slices.
  8. const DEFAULT_CONCURRENT_TASK_NUMBER = 3;
  9. private $timeoutMs; // int: timeout in milliseconds for each http request.
  10. private $maxRetryCount; // int: max retry count on failure.
  11. private $errorCode; // int: last error code.
  12. private $errorMessage; // string: last error message.
  13. private $requestId; // string: request id for last http request.
  14. private $signature; // string: signature for auth.
  15. private $srcFpath; // string: source file path for uploading.
  16. private $url; // string: destination url for uploading.
  17. private $fileSize; // int: source file size.
  18. private $sliceSize; // int: slice size for each upload.
  19. private $session; // string: session for each upload transaction.
  20. private $concurrentTaskNumber; // int: concurrent uploading task number.
  21. private $offset; // int: current uploading offset.
  22. private $libcurlWrapper; // LibcurlWrapper: curl wrapper for sending multi http request concurrently.
  23. private $httpClient;
  24. private $accessUrl; // string: access url.
  25. private $resourcePath; // string: resource path.
  26. private $sourceUrl; // string: source url.
  27. /**
  28. * timeoutMs: max timeout in milliseconds for each http request.
  29. * maxRetryCount: max retry count for uploading each slice on error.
  30. */
  31. public function __construct($timeoutMs, $maxRetryCount) {
  32. $this->timeoutMs = $timeoutMs;
  33. $this->maxRetryCount = $maxRetryCount;
  34. $this->errorCode = Api::COSAPI_SUCCESS;
  35. $this->errorMessage = '';
  36. $this->concurrentTaskNumber = self::DEFAULT_CONCURRENT_TASK_NUMBER;
  37. $this->offset = 0;
  38. $this->libcurlWrapper = new LibcurlWrapper();
  39. $this->httpClient = new HttpClient();
  40. }
  41. public function __destruct() {
  42. }
  43. public function getLastErrorCode() {
  44. return $this->errorCode;
  45. }
  46. public function getLastErrorMessage() {
  47. return $this->errorMessage;
  48. }
  49. public function getRequestId() {
  50. return $this->requestId;
  51. }
  52. public function getAccessUrl() {
  53. return $this->accessUrl;
  54. }
  55. public function getResourcePath() {
  56. return $this->resourcePath;
  57. }
  58. public function getSourceUrl() {
  59. return $this->sourceUrl;
  60. }
  61. /**
  62. * Return true on success and return false on failure.
  63. */
  64. public function initUploading(
  65. $signature, $srcFpath, $url, $fileSize, $sliceSize, $bizAttr, $insertOnly) {
  66. $this->signature = $signature;
  67. $this->srcFpath = $srcFpath;
  68. $this->url = $url;
  69. $this->fileSize = $fileSize;
  70. $this->sliceSize = $sliceSize;
  71. // Clear error so caller can successfully retry.
  72. $this->clearError();
  73. $request = array(
  74. 'url' => $url,
  75. 'method' => 'post',
  76. 'timeout' => $this->timeoutMs / 1000,
  77. 'data' => array(
  78. 'op' => 'upload_slice_init',
  79. 'filesize' => $fileSize,
  80. 'slice_size' => $sliceSize,
  81. 'insertOnly' => $insertOnly,
  82. ),
  83. 'header' => array(
  84. 'Authorization: ' . $signature,
  85. ),
  86. );
  87. if (isset($bizAttr) && strlen($bizAttr)) {
  88. $request['data']['biz_attr'] = $bizAttr;
  89. }
  90. $response = $this->sendRequest($request);
  91. if ($response === false) {
  92. return false;
  93. }
  94. $this->session = $response['data']['session'];
  95. if (isset($response['data']['slice_size'])) {
  96. $this->sliceSize = $response['data']['slice_size'];
  97. }
  98. if (isset($response['data']['serial_upload']) && $response['data']['serial_upload'] == 1) {
  99. $this->concurrentTaskNumber = 1;
  100. }
  101. return true;
  102. }
  103. /**
  104. * Return true on success and return false on failure.
  105. */
  106. public function performUploading() {
  107. for ($i = 0; $i < $this->concurrentTaskNumber; ++$i) {
  108. if ($this->offset >= $this->fileSize) {
  109. break;
  110. }
  111. $sliceContent = file_get_contents($this->srcFpath, false, null, $this->offset, $this->sliceSize);
  112. if ($sliceContent === false) {
  113. $this->setError(COSAPI_PARAMS_ERROR, 'read file ' . $this->srcFpath . ' error');
  114. return false;
  115. }
  116. $request = new HttpRequest();
  117. $request->timeoutMs = $this->timeoutMs;
  118. $request->url = $this->url;
  119. $request->method = 'POST';
  120. $request->customHeaders = array(
  121. 'Authorization: ' . $this->signature,
  122. );
  123. $request->dataToPost = array(
  124. 'op' => 'upload_slice_data',
  125. 'session' => $this->session,
  126. 'offset' => $this->offset,
  127. 'filecontent' => $sliceContent,
  128. 'datamd5' => md5($sliceContent),
  129. );
  130. $request->userData = array(
  131. 'retryCount' => 0,
  132. );
  133. $this->libcurlWrapper->startSendingRequest($request, array($this, 'uploadCallback'));
  134. $this->offset += $this->sliceSize;
  135. }
  136. $this->libcurlWrapper->performSendingRequest();
  137. if ($this->errorCode !== Api::COSAPI_SUCCESS) {
  138. return false;
  139. }
  140. return true;
  141. }
  142. /**
  143. * Return true on success and return false on failure.
  144. */
  145. public function finishUploading() {
  146. $request = array(
  147. 'url' => $this->url,
  148. 'method' => 'post',
  149. 'timeout' => $this->timeoutMs / 1000,
  150. 'data' => array(
  151. 'op' => 'upload_slice_finish',
  152. 'session' => $this->session,
  153. 'filesize' => $this->fileSize,
  154. ),
  155. 'header' => array(
  156. 'Authorization: ' . $this->signature,
  157. ),
  158. );
  159. $response = $this->sendRequest($request);
  160. if ($response === false) {
  161. return false;
  162. }
  163. $this->accessUrl = $response['data']['access_url'];
  164. $this->resourcePath = $response['data']['resource_path'];
  165. $this->sourceUrl = $response['data']['source_url'];
  166. return true;
  167. }
  168. private function sendRequest($request) {
  169. $response = $this->httpClient->sendRequest($request);
  170. if ($response === false) {
  171. $this->setError(Api::COSAPI_NETWORK_ERROR, 'network error');
  172. return false;
  173. }
  174. $responseJson = json_decode($response, true);
  175. if ($responseJson === NULL) {
  176. $this->setError(Api::COSAPI_NETWORK_ERROR, 'network error');
  177. return false;
  178. }
  179. $this->requestId = $responseJson['request_id'];
  180. if ($responseJson['code'] != 0) {
  181. $this->setError($responseJson['code'], $responseJson['message']);
  182. return false;
  183. }
  184. return $responseJson;
  185. }
  186. private function clearError() {
  187. $this->errorCode = Api::COSAPI_SUCCESS;
  188. $this->errorMessage = 'success';
  189. }
  190. private function setError($errorCode, $errorMessage) {
  191. $this->errorCode = $errorCode;
  192. $this->errorMessage = $errorMessage;
  193. }
  194. public function uploadCallback($request, $response) {
  195. if ($this->errorCode !== Api::COSAPI_SUCCESS) {
  196. return;
  197. }
  198. $requestErrorCode = Api::COSAPI_SUCCESS;
  199. $requestErrorMessage = 'success';
  200. $retryCount = $request->userData['retryCount'];
  201. $responseJson = json_decode($response->body, true);
  202. if ($responseJson === NULL) {
  203. $requestErrorCode = Api::COSAPI_NETWORK_ERROR;
  204. $requestErrorMessage = 'network error';
  205. }
  206. if ($response->curlErrorCode !== CURLE_OK) {
  207. $requestErrorCode = Api::COSAPI_NETWORK_ERROR;
  208. $requestErrorMessage = 'network error: curl errno ' . $response->curlErrorCode;
  209. }
  210. $this->requestId = $responseJson['request_id'];
  211. if ($responseJson['code'] != 0) {
  212. $requestErrorCode = $responseJson['code'];
  213. $requestErrorMessage = $responseJson['message'];
  214. }
  215. if (isset($responseJson['data']['datamd5']) &&
  216. $responseJson['data']['datamd5'] !== $request->dataToPost['datamd5']) {
  217. $requestErrorCode = Api::COSAPI_INTEGRITY_ERROR;
  218. $requestErrorMessage = 'cosapi integrity error';
  219. }
  220. if ($requestErrorCode !== Api::COSAPI_SUCCESS) {
  221. if ($retryCount >= $this->maxRetryCount) {
  222. $this->setError($requestErrorCode, $requestErrorMessage);
  223. } else {
  224. $request->userData['retryCount'] += 1;
  225. $this->libcurlWrapper->startSendingRequest($request, array($this, 'uploadCallback'));
  226. }
  227. return;
  228. }
  229. if ($this->offset >= $this->fileSize) {
  230. return;
  231. }
  232. // Send next slice.
  233. $nextSliceContent = file_get_contents($this->srcFpath, false, null, $this->offset, $this->sliceSize);
  234. if ($nextSliceContent === false) {
  235. $this->setError(COSAPI_PARAMS_ERROR, 'read file ' . $this->srcFpath . ' error');
  236. return;
  237. }
  238. $nextSliceRequest = new HttpRequest();
  239. $nextSliceRequest->timeoutMs = $this->timeoutMs;
  240. $nextSliceRequest->url = $this->url;
  241. $nextSliceRequest->method = 'POST';
  242. $nextSliceRequest->customHeaders = array(
  243. 'Authorization: ' . $this->signature,
  244. );
  245. $nextSliceRequest->dataToPost = array(
  246. 'op' => 'upload_slice_data',
  247. 'session' => $this->session,
  248. 'offset' => $this->offset,
  249. 'filecontent' => $nextSliceContent,
  250. 'datamd5' => md5($nextSliceContent),
  251. );
  252. $nextSliceRequest->userData = array(
  253. 'retryCount' => 0,
  254. );
  255. $this->libcurlWrapper->startSendingRequest($nextSliceRequest, array($this, 'uploadCallback'));
  256. $this->offset += $this->sliceSize;
  257. }
  258. }