Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 176 additions & 17 deletions src/Appwrite/Services/Functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading