diff --git a/src/Appwrite/Services/Functions.php b/src/Appwrite/Services/Functions.php index 7d44d39..13eb02c 100644 --- a/src/Appwrite/Services/Functions.php +++ b/src/Appwrite/Services/Functions.php @@ -635,35 +635,194 @@ public function createDeployment(string $functionId, InputFile $code, bool $acti $handle = @fopen($code->getPath(), "rb"); } + $uploadId = ''; + $totalChunks = (int) ceil($size / Client::CHUNK_SIZE); + $chunks = []; $start = $counter * Client::CHUNK_SIZE; while ($start < $size) { - $chunk = ''; + $chunks[] = [ + 'index' => $counter, + 'start' => $start, + 'end' => min($start + Client::CHUNK_SIZE, $size), + ]; + $counter++; + $start += Client::CHUNK_SIZE; + } + + $readChunk = function(int $start, int $end) use ($handle, $code) { if(!empty($handle)) { fseek($handle, $start); - $chunk = @fread($handle, Client::CHUNK_SIZE); - } else { - $chunk = substr($code->getData(), $start, Client::CHUNK_SIZE); + return @fread($handle, $end - $start); } - $apiParams['code'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($chunk), $mimeType, $postedName); - $apiHeaders['content-range'] = 'bytes ' . ($counter * Client::CHUNK_SIZE) . '-' . min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE) - 1), $size - 1) . '/' . $size; - if(!empty($id)) { - $apiHeaders['x-appwrite-id'] = $id; + + return substr($code->getData(), $start, $end - $start); + }; + + $uploadChunk = function(array $chunk, string $currentUploadId = '') use ($readChunk, $apiPath, $apiHeaders, $apiParams, $mimeType, $postedName, $size) { + $chunkParams = $apiParams; + $chunkHeaders = $apiHeaders; + $data = $readChunk($chunk['start'], $chunk['end']); + $chunkParams['code'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($data), $mimeType, $postedName); + $chunkHeaders['content-range'] = 'bytes ' . $chunk['start'] . '-' . ($chunk['end'] - 1) . '/' . $size; + if(!empty($currentUploadId)) { + $chunkHeaders['x-appwrite-id'] = $currentUploadId; } - $response = $this->client->call(Client::METHOD_POST, $apiPath, $apiHeaders, $apiParams); - $counter++; - $start += Client::CHUNK_SIZE; - if(empty($id)) { - $id = $response['$id']; + + return $this->client->call(Client::METHOD_POST, $apiPath, $chunkHeaders, $chunkParams); + }; + + $isUploadComplete = function($chunkResponse) use ($totalChunks): bool { + if(!is_array($chunkResponse) || !isset($chunkResponse['chunksUploaded'])) { + return false; } + + return (int) $chunkResponse['chunksUploaded'] >= (int) ($chunkResponse['chunksTotal'] ?? $totalChunks); + }; + + if (!empty($chunks)) { + $response = $uploadChunk($chunks[0], $uploadId); + if(empty($uploadId)) { + $uploadId = $response['$id']; + } + $completedCount = $chunks[0]['index'] + 1; + $uploadedSize = $chunks[0]['end']; if($onProgress !== null) { $onProgress([ '$id' => $response['$id'], - 'progress' => min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE)), $size) / $size * 100, - 'sizeUploaded' => min($counter * Client::CHUNK_SIZE), - 'chunksTotal' => $response['chunksTotal'], - 'chunksUploaded' => $response['chunksUploaded'], + 'progress' => $uploadedSize / $size * 100, + 'sizeUploaded' => $uploadedSize, + 'chunksTotal' => $totalChunks, + 'chunksUploaded' => $completedCount, ]); } + + $remainingChunks = array_slice($chunks, 1); + $clientConfig = \Closure::bind(function() { + if (property_exists($this, 'key') && $this->key !== null) { + $this->headers['authorization'] = $this->getAuthorization(); + } + + return [$this->endpoint, $this->headers, $this->selfSigned, $this->timeout, $this->connectTimeout]; + }, $this->client, Client::class); + $flattenParams = \Closure::bind(function(array $params): array { + return $this->flatten($params); + }, $this->client, Client::class); + [$endpoint, $globalHeaders, $selfSigned, $timeout, $connectTimeout] = $clientConfig(); + $responseHeaders = []; + $lastResponse = $response; + $completedResponse = null; + + $makeHandle = function(array $chunk) use ($readChunk, $apiPath, $apiHeaders, $apiParams, $mimeType, $postedName, $size, $uploadId, $endpoint, $globalHeaders, $selfSigned, $timeout, $connectTimeout, $flattenParams, &$responseHeaders) { + $chunkParams = $apiParams; + $chunkHeaders = array_merge($globalHeaders, $apiHeaders); + $data = $readChunk($chunk['start'], $chunk['end']); + $chunkParams['code'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($data), $mimeType, $postedName); + $chunkHeaders['content-range'] = 'bytes ' . $chunk['start'] . '-' . ($chunk['end'] - 1) . '/' . $size; + if(!empty($uploadId)) { + $chunkHeaders['x-appwrite-id'] = $uploadId; + } + + $headers = []; + foreach ($chunkHeaders as $key => $value) { + $headers[] = $key . ':' . $value; + } + + $ch = curl_init($endpoint . $apiPath); + $responseHeaders[spl_object_id($ch)] = []; + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, Client::METHOD_POST); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERAGENT, php_uname('s') . '-' . php_uname('r') . ':php-' . phpversion()); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $flattenParams($chunkParams)); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$responseHeaders) { + $length = strlen($header); + $header = explode(':', strtolower($header), 2); + if (count($header) >= 2) { + $responseHeaders[spl_object_id($curl)][strtolower(trim($header[0]))] = trim($header[1]); + } + + return $length; + }); + if($selfSigned) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + } + if($timeout !== null) { + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + } + if($connectTimeout !== null) { + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $connectTimeout); + } + + return $ch; + }; + + $nextChunk = 0; + while ($nextChunk < count($remainingChunks)) { + $multiHandle = curl_multi_init(); + $handles = []; + for ($i = 0; $i < 8 && $nextChunk < count($remainingChunks); $i++, $nextChunk++) { + $chunk = $remainingChunks[$nextChunk]; + $ch = $makeHandle($chunk); + $handles[spl_object_id($ch)] = ['handle' => $ch, 'chunk' => $chunk]; + curl_multi_add_handle($multiHandle, $ch); + } + + try { + do { + $status = curl_multi_exec($multiHandle, $active); + if ($active) { + curl_multi_select($multiHandle); + } + } while ($active && ($status == CURLM_OK || $status == CURLM_CALL_MULTI_PERFORM)); + + foreach ($handles as $handleInfo) { + $ch = $handleInfo['handle']; + $body = curl_multi_getcontent($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = $responseHeaders[spl_object_id($ch)]['content-type'] ?? ''; + + if (curl_errno($ch)) { + throw new AppwriteException(curl_error($ch), $statusCode, '', $body); + } + + $chunkResponse = str_starts_with($contentType, 'application/json') ? json_decode($body, true) : $body; + + if($statusCode >= 400) { + if(is_array($chunkResponse)) { + throw new AppwriteException($chunkResponse['message'], $statusCode, $chunkResponse['type'] ?? '', json_encode($chunkResponse)); + } + + throw new AppwriteException($chunkResponse, $statusCode, '', $chunkResponse); + } + + $completedCount++; + $uploadedSize += $handleInfo['chunk']['end'] - $handleInfo['chunk']['start']; + $lastResponse = $chunkResponse; + if($isUploadComplete($chunkResponse)) { + $completedResponse = $chunkResponse; + } + if($onProgress !== null) { + $onProgress([ + '$id' => $uploadId, + 'progress' => $uploadedSize / $size * 100, + 'sizeUploaded' => $uploadedSize, + 'chunksTotal' => $totalChunks, + 'chunksUploaded' => $completedCount, + ]); + } + } + } finally { + foreach ($handles as $handleInfo) { + curl_multi_remove_handle($multiHandle, $handleInfo['handle']); + curl_close($handleInfo['handle']); + } + curl_multi_close($multiHandle); + } + } + $response = $completedResponse ?? $lastResponse; + } if(!empty($handle)) { @fclose($handle); diff --git a/src/Appwrite/Services/Sites.php b/src/Appwrite/Services/Sites.php index 660a1bb..cf69e3c 100644 --- a/src/Appwrite/Services/Sites.php +++ b/src/Appwrite/Services/Sites.php @@ -641,35 +641,194 @@ public function createDeployment(string $siteId, InputFile $code, ?string $insta $handle = @fopen($code->getPath(), "rb"); } + $uploadId = ''; + $totalChunks = (int) ceil($size / Client::CHUNK_SIZE); + $chunks = []; $start = $counter * Client::CHUNK_SIZE; while ($start < $size) { - $chunk = ''; + $chunks[] = [ + 'index' => $counter, + 'start' => $start, + 'end' => min($start + Client::CHUNK_SIZE, $size), + ]; + $counter++; + $start += Client::CHUNK_SIZE; + } + + $readChunk = function(int $start, int $end) use ($handle, $code) { if(!empty($handle)) { fseek($handle, $start); - $chunk = @fread($handle, Client::CHUNK_SIZE); - } else { - $chunk = substr($code->getData(), $start, Client::CHUNK_SIZE); + return @fread($handle, $end - $start); } - $apiParams['code'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($chunk), $mimeType, $postedName); - $apiHeaders['content-range'] = 'bytes ' . ($counter * Client::CHUNK_SIZE) . '-' . min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE) - 1), $size - 1) . '/' . $size; - if(!empty($id)) { - $apiHeaders['x-appwrite-id'] = $id; + + return substr($code->getData(), $start, $end - $start); + }; + + $uploadChunk = function(array $chunk, string $currentUploadId = '') use ($readChunk, $apiPath, $apiHeaders, $apiParams, $mimeType, $postedName, $size) { + $chunkParams = $apiParams; + $chunkHeaders = $apiHeaders; + $data = $readChunk($chunk['start'], $chunk['end']); + $chunkParams['code'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($data), $mimeType, $postedName); + $chunkHeaders['content-range'] = 'bytes ' . $chunk['start'] . '-' . ($chunk['end'] - 1) . '/' . $size; + if(!empty($currentUploadId)) { + $chunkHeaders['x-appwrite-id'] = $currentUploadId; } - $response = $this->client->call(Client::METHOD_POST, $apiPath, $apiHeaders, $apiParams); - $counter++; - $start += Client::CHUNK_SIZE; - if(empty($id)) { - $id = $response['$id']; + + return $this->client->call(Client::METHOD_POST, $apiPath, $chunkHeaders, $chunkParams); + }; + + $isUploadComplete = function($chunkResponse) use ($totalChunks): bool { + if(!is_array($chunkResponse) || !isset($chunkResponse['chunksUploaded'])) { + return false; } + + return (int) $chunkResponse['chunksUploaded'] >= (int) ($chunkResponse['chunksTotal'] ?? $totalChunks); + }; + + if (!empty($chunks)) { + $response = $uploadChunk($chunks[0], $uploadId); + if(empty($uploadId)) { + $uploadId = $response['$id']; + } + $completedCount = $chunks[0]['index'] + 1; + $uploadedSize = $chunks[0]['end']; if($onProgress !== null) { $onProgress([ '$id' => $response['$id'], - 'progress' => min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE)), $size) / $size * 100, - 'sizeUploaded' => min($counter * Client::CHUNK_SIZE), - 'chunksTotal' => $response['chunksTotal'], - 'chunksUploaded' => $response['chunksUploaded'], + 'progress' => $uploadedSize / $size * 100, + 'sizeUploaded' => $uploadedSize, + 'chunksTotal' => $totalChunks, + 'chunksUploaded' => $completedCount, ]); } + + $remainingChunks = array_slice($chunks, 1); + $clientConfig = \Closure::bind(function() { + if (property_exists($this, 'key') && $this->key !== null) { + $this->headers['authorization'] = $this->getAuthorization(); + } + + return [$this->endpoint, $this->headers, $this->selfSigned, $this->timeout, $this->connectTimeout]; + }, $this->client, Client::class); + $flattenParams = \Closure::bind(function(array $params): array { + return $this->flatten($params); + }, $this->client, Client::class); + [$endpoint, $globalHeaders, $selfSigned, $timeout, $connectTimeout] = $clientConfig(); + $responseHeaders = []; + $lastResponse = $response; + $completedResponse = null; + + $makeHandle = function(array $chunk) use ($readChunk, $apiPath, $apiHeaders, $apiParams, $mimeType, $postedName, $size, $uploadId, $endpoint, $globalHeaders, $selfSigned, $timeout, $connectTimeout, $flattenParams, &$responseHeaders) { + $chunkParams = $apiParams; + $chunkHeaders = array_merge($globalHeaders, $apiHeaders); + $data = $readChunk($chunk['start'], $chunk['end']); + $chunkParams['code'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($data), $mimeType, $postedName); + $chunkHeaders['content-range'] = 'bytes ' . $chunk['start'] . '-' . ($chunk['end'] - 1) . '/' . $size; + if(!empty($uploadId)) { + $chunkHeaders['x-appwrite-id'] = $uploadId; + } + + $headers = []; + foreach ($chunkHeaders as $key => $value) { + $headers[] = $key . ':' . $value; + } + + $ch = curl_init($endpoint . $apiPath); + $responseHeaders[spl_object_id($ch)] = []; + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, Client::METHOD_POST); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERAGENT, php_uname('s') . '-' . php_uname('r') . ':php-' . phpversion()); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $flattenParams($chunkParams)); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$responseHeaders) { + $length = strlen($header); + $header = explode(':', strtolower($header), 2); + if (count($header) >= 2) { + $responseHeaders[spl_object_id($curl)][strtolower(trim($header[0]))] = trim($header[1]); + } + + return $length; + }); + if($selfSigned) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + } + if($timeout !== null) { + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + } + if($connectTimeout !== null) { + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $connectTimeout); + } + + return $ch; + }; + + $nextChunk = 0; + while ($nextChunk < count($remainingChunks)) { + $multiHandle = curl_multi_init(); + $handles = []; + for ($i = 0; $i < 8 && $nextChunk < count($remainingChunks); $i++, $nextChunk++) { + $chunk = $remainingChunks[$nextChunk]; + $ch = $makeHandle($chunk); + $handles[spl_object_id($ch)] = ['handle' => $ch, 'chunk' => $chunk]; + curl_multi_add_handle($multiHandle, $ch); + } + + try { + do { + $status = curl_multi_exec($multiHandle, $active); + if ($active) { + curl_multi_select($multiHandle); + } + } while ($active && ($status == CURLM_OK || $status == CURLM_CALL_MULTI_PERFORM)); + + foreach ($handles as $handleInfo) { + $ch = $handleInfo['handle']; + $body = curl_multi_getcontent($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = $responseHeaders[spl_object_id($ch)]['content-type'] ?? ''; + + if (curl_errno($ch)) { + throw new AppwriteException(curl_error($ch), $statusCode, '', $body); + } + + $chunkResponse = str_starts_with($contentType, 'application/json') ? json_decode($body, true) : $body; + + if($statusCode >= 400) { + if(is_array($chunkResponse)) { + throw new AppwriteException($chunkResponse['message'], $statusCode, $chunkResponse['type'] ?? '', json_encode($chunkResponse)); + } + + throw new AppwriteException($chunkResponse, $statusCode, '', $chunkResponse); + } + + $completedCount++; + $uploadedSize += $handleInfo['chunk']['end'] - $handleInfo['chunk']['start']; + $lastResponse = $chunkResponse; + if($isUploadComplete($chunkResponse)) { + $completedResponse = $chunkResponse; + } + if($onProgress !== null) { + $onProgress([ + '$id' => $uploadId, + 'progress' => $uploadedSize / $size * 100, + 'sizeUploaded' => $uploadedSize, + 'chunksTotal' => $totalChunks, + 'chunksUploaded' => $completedCount, + ]); + } + } + } finally { + foreach ($handles as $handleInfo) { + curl_multi_remove_handle($multiHandle, $handleInfo['handle']); + curl_close($handleInfo['handle']); + } + curl_multi_close($multiHandle); + } + } + $response = $completedResponse ?? $lastResponse; + } if(!empty($handle)) { @fclose($handle); diff --git a/src/Appwrite/Services/Storage.php b/src/Appwrite/Services/Storage.php index a866114..5fbdb21 100644 --- a/src/Appwrite/Services/Storage.php +++ b/src/Appwrite/Services/Storage.php @@ -443,35 +443,195 @@ public function createFile(string $bucketId, string $fileId, InputFile $file, ?a $handle = @fopen($file->getPath(), "rb"); } + $uploadId = ''; + $uploadId = $fileId ?? ''; + $totalChunks = (int) ceil($size / Client::CHUNK_SIZE); + $chunks = []; $start = $counter * Client::CHUNK_SIZE; while ($start < $size) { - $chunk = ''; + $chunks[] = [ + 'index' => $counter, + 'start' => $start, + 'end' => min($start + Client::CHUNK_SIZE, $size), + ]; + $counter++; + $start += Client::CHUNK_SIZE; + } + + $readChunk = function(int $start, int $end) use ($handle, $file) { if(!empty($handle)) { fseek($handle, $start); - $chunk = @fread($handle, Client::CHUNK_SIZE); - } else { - $chunk = substr($file->getData(), $start, Client::CHUNK_SIZE); + return @fread($handle, $end - $start); } - $apiParams['file'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($chunk), $mimeType, $postedName); - $apiHeaders['content-range'] = 'bytes ' . ($counter * Client::CHUNK_SIZE) . '-' . min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE) - 1), $size - 1) . '/' . $size; - if(!empty($id)) { - $apiHeaders['x-appwrite-id'] = $id; + + return substr($file->getData(), $start, $end - $start); + }; + + $uploadChunk = function(array $chunk, string $currentUploadId = '') use ($readChunk, $apiPath, $apiHeaders, $apiParams, $mimeType, $postedName, $size) { + $chunkParams = $apiParams; + $chunkHeaders = $apiHeaders; + $data = $readChunk($chunk['start'], $chunk['end']); + $chunkParams['file'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($data), $mimeType, $postedName); + $chunkHeaders['content-range'] = 'bytes ' . $chunk['start'] . '-' . ($chunk['end'] - 1) . '/' . $size; + if(!empty($currentUploadId)) { + $chunkHeaders['x-appwrite-id'] = $currentUploadId; } - $response = $this->client->call(Client::METHOD_POST, $apiPath, $apiHeaders, $apiParams); - $counter++; - $start += Client::CHUNK_SIZE; - if(empty($id)) { - $id = $response['$id']; + + return $this->client->call(Client::METHOD_POST, $apiPath, $chunkHeaders, $chunkParams); + }; + + $isUploadComplete = function($chunkResponse) use ($totalChunks): bool { + if(!is_array($chunkResponse) || !isset($chunkResponse['chunksUploaded'])) { + return false; } + + return (int) $chunkResponse['chunksUploaded'] >= (int) ($chunkResponse['chunksTotal'] ?? $totalChunks); + }; + + if (!empty($chunks)) { + $response = $uploadChunk($chunks[0], $uploadId); + if(empty($uploadId)) { + $uploadId = $response['$id']; + } + $completedCount = $chunks[0]['index'] + 1; + $uploadedSize = $chunks[0]['end']; if($onProgress !== null) { $onProgress([ '$id' => $response['$id'], - 'progress' => min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE)), $size) / $size * 100, - 'sizeUploaded' => min($counter * Client::CHUNK_SIZE), - 'chunksTotal' => $response['chunksTotal'], - 'chunksUploaded' => $response['chunksUploaded'], + 'progress' => $uploadedSize / $size * 100, + 'sizeUploaded' => $uploadedSize, + 'chunksTotal' => $totalChunks, + 'chunksUploaded' => $completedCount, ]); } + + $remainingChunks = array_slice($chunks, 1); + $clientConfig = \Closure::bind(function() { + if (property_exists($this, 'key') && $this->key !== null) { + $this->headers['authorization'] = $this->getAuthorization(); + } + + return [$this->endpoint, $this->headers, $this->selfSigned, $this->timeout, $this->connectTimeout]; + }, $this->client, Client::class); + $flattenParams = \Closure::bind(function(array $params): array { + return $this->flatten($params); + }, $this->client, Client::class); + [$endpoint, $globalHeaders, $selfSigned, $timeout, $connectTimeout] = $clientConfig(); + $responseHeaders = []; + $lastResponse = $response; + $completedResponse = null; + + $makeHandle = function(array $chunk) use ($readChunk, $apiPath, $apiHeaders, $apiParams, $mimeType, $postedName, $size, $uploadId, $endpoint, $globalHeaders, $selfSigned, $timeout, $connectTimeout, $flattenParams, &$responseHeaders) { + $chunkParams = $apiParams; + $chunkHeaders = array_merge($globalHeaders, $apiHeaders); + $data = $readChunk($chunk['start'], $chunk['end']); + $chunkParams['file'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($data), $mimeType, $postedName); + $chunkHeaders['content-range'] = 'bytes ' . $chunk['start'] . '-' . ($chunk['end'] - 1) . '/' . $size; + if(!empty($uploadId)) { + $chunkHeaders['x-appwrite-id'] = $uploadId; + } + + $headers = []; + foreach ($chunkHeaders as $key => $value) { + $headers[] = $key . ':' . $value; + } + + $ch = curl_init($endpoint . $apiPath); + $responseHeaders[spl_object_id($ch)] = []; + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, Client::METHOD_POST); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERAGENT, php_uname('s') . '-' . php_uname('r') . ':php-' . phpversion()); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $flattenParams($chunkParams)); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$responseHeaders) { + $length = strlen($header); + $header = explode(':', strtolower($header), 2); + if (count($header) >= 2) { + $responseHeaders[spl_object_id($curl)][strtolower(trim($header[0]))] = trim($header[1]); + } + + return $length; + }); + if($selfSigned) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + } + if($timeout !== null) { + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + } + if($connectTimeout !== null) { + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $connectTimeout); + } + + return $ch; + }; + + $nextChunk = 0; + while ($nextChunk < count($remainingChunks)) { + $multiHandle = curl_multi_init(); + $handles = []; + for ($i = 0; $i < 8 && $nextChunk < count($remainingChunks); $i++, $nextChunk++) { + $chunk = $remainingChunks[$nextChunk]; + $ch = $makeHandle($chunk); + $handles[spl_object_id($ch)] = ['handle' => $ch, 'chunk' => $chunk]; + curl_multi_add_handle($multiHandle, $ch); + } + + try { + do { + $status = curl_multi_exec($multiHandle, $active); + if ($active) { + curl_multi_select($multiHandle); + } + } while ($active && ($status == CURLM_OK || $status == CURLM_CALL_MULTI_PERFORM)); + + foreach ($handles as $handleInfo) { + $ch = $handleInfo['handle']; + $body = curl_multi_getcontent($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = $responseHeaders[spl_object_id($ch)]['content-type'] ?? ''; + + if (curl_errno($ch)) { + throw new AppwriteException(curl_error($ch), $statusCode, '', $body); + } + + $chunkResponse = str_starts_with($contentType, 'application/json') ? json_decode($body, true) : $body; + + if($statusCode >= 400) { + if(is_array($chunkResponse)) { + throw new AppwriteException($chunkResponse['message'], $statusCode, $chunkResponse['type'] ?? '', json_encode($chunkResponse)); + } + + throw new AppwriteException($chunkResponse, $statusCode, '', $chunkResponse); + } + + $completedCount++; + $uploadedSize += $handleInfo['chunk']['end'] - $handleInfo['chunk']['start']; + $lastResponse = $chunkResponse; + if($isUploadComplete($chunkResponse)) { + $completedResponse = $chunkResponse; + } + if($onProgress !== null) { + $onProgress([ + '$id' => $uploadId, + 'progress' => $uploadedSize / $size * 100, + 'sizeUploaded' => $uploadedSize, + 'chunksTotal' => $totalChunks, + 'chunksUploaded' => $completedCount, + ]); + } + } + } finally { + foreach ($handles as $handleInfo) { + curl_multi_remove_handle($multiHandle, $handleInfo['handle']); + curl_close($handleInfo['handle']); + } + curl_multi_close($multiHandle); + } + } + $response = $completedResponse ?? $lastResponse; + } if(!empty($handle)) { @fclose($handle);