-
Notifications
You must be signed in to change notification settings - Fork 58
Split chunked upload phases #165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -167,7 +167,64 @@ public static function setRetryDelay(int $delay): void | |
| */ | ||
| public function upload(string $source, string $path, int $chunk = 1, int $chunks = 1, array &$metadata = []): int | ||
| { | ||
| return $this->uploadData(\file_get_contents($source), $path, \mime_content_type($source), $chunk, $chunks, $metadata); | ||
| $contentType = \mime_content_type($source) ?: ''; | ||
| $this->prepareUpload($path, $contentType, $chunks, $metadata); | ||
| $chunksReceived = $this->uploadChunk($source, $path, $chunk, $chunks, $metadata); | ||
|
|
||
| if ($chunks === $chunksReceived) { | ||
| $this->finalizeUpload($path, $chunks, $metadata); | ||
| } | ||
|
|
||
| return $chunksReceived; | ||
| } | ||
|
|
||
| public function prepareUpload(string $path, string $contentType, int $chunks = 1, array &$metadata = []): void | ||
| { | ||
| $metadata['parts'] ??= []; | ||
| $metadata['chunks'] ??= 0; | ||
| $metadata['content_type'] ??= $contentType; | ||
|
|
||
| if ($chunks === 1 || ! empty($metadata['uploadId'])) { | ||
| return; | ||
| } | ||
|
|
||
| $metadata['uploadId'] = $this->createMultipartUpload($path, $contentType); | ||
| } | ||
|
|
||
| public function uploadChunk(string $source, string $path, int $chunk = 1, int $chunks = 1, array &$metadata = []): int | ||
| { | ||
| $data = \file_get_contents($source); | ||
| if ($data === false) { | ||
| throw new Exception('Can\'t read file '.$source); | ||
| } | ||
|
|
||
| return $this->uploadChunkData($data, $path, $metadata['content_type'] ?? (\mime_content_type($source) ?: ''), $chunk, $chunks, $metadata); | ||
| } | ||
|
|
||
| public function finalizeUpload(string $path, int $chunks = 1, array &$metadata = []): bool | ||
| { | ||
| if ($this->exists($path)) { | ||
| return true; | ||
| } | ||
|
|
||
| if ($chunks === 1) { | ||
| return false; | ||
| } | ||
|
|
||
| if (empty($metadata['uploadId'])) { | ||
| throw new Exception('Missing multipart upload ID'); | ||
| } | ||
|
|
||
| $metadata['parts'] ??= []; | ||
| for ($i = 1; $i <= $chunks; $i++) { | ||
| if (! array_key_exists($i, $metadata['parts'])) { | ||
| throw new Exception('Missing chunk '.$i); | ||
| } | ||
| } | ||
|
|
||
| $this->completeMultipartUpload($path, $metadata['uploadId'], $metadata['parts']); | ||
|
|
||
| return true; | ||
| } | ||
|
Comment on lines
+204
to
228
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| /** | ||
|
|
@@ -183,38 +240,40 @@ public function upload(string $source, string $path, int $chunk = 1, int $chunks | |
| * @throws Exception | ||
| */ | ||
| public function uploadData(string $data, string $path, string $contentType, int $chunk = 1, int $chunks = 1, array &$metadata = []): int | ||
| { | ||
| $this->prepareUpload($path, $contentType, $chunks, $metadata); | ||
| $chunksReceived = $this->uploadChunkData($data, $path, $contentType, $chunk, $chunks, $metadata); | ||
|
|
||
| if ($chunks === $chunksReceived) { | ||
| $this->finalizeUpload($path, $chunks, $metadata); | ||
| } | ||
|
|
||
| return $chunksReceived; | ||
| } | ||
|
Comment on lines
242
to
+252
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Both |
||
|
|
||
| private function uploadChunkData(string $data, string $path, string $contentType, int $chunk = 1, int $chunks = 1, array &$metadata = []): int | ||
| { | ||
| if ($chunk == 1 && $chunks == 1) { | ||
| return $this->write($path, $data, $contentType); | ||
| $this->write($path, $data, $contentType); | ||
| $metadata['parts'][$chunk] = true; | ||
| $metadata['chunks'] = 1; | ||
|
|
||
| return 1; | ||
| } | ||
| $uploadId = $metadata['uploadId'] ?? null; | ||
| if (empty($uploadId)) { | ||
| $uploadId = $this->createMultipartUpload($path, $contentType); | ||
| $metadata['uploadId'] = $uploadId; | ||
|
|
||
| if (empty($metadata['uploadId'])) { | ||
| throw new Exception('Missing multipart upload ID'); | ||
| } | ||
|
|
||
| $metadata['parts'] ??= []; | ||
| $metadata['chunks'] ??= 0; | ||
|
|
||
| $etag = $this->uploadPart($data, $path, $contentType, $chunk, $uploadId); | ||
| $etag = $this->uploadPart($data, $path, $contentType, $chunk, $metadata['uploadId']); | ||
| // skip incrementing if the chunk was re-uploaded | ||
| if (! array_key_exists($chunk, $metadata['parts'])) { | ||
| $metadata['chunks']++; | ||
| } | ||
| $metadata['parts'][$chunk] = $etag; | ||
| if ($metadata['chunks'] == $chunks) { | ||
| $headers = $this->headers; | ||
| $amzHeaders = $this->amzHeaders; | ||
|
|
||
| if ($this->exists($path)) { | ||
| return $metadata['chunks']; | ||
| } | ||
|
|
||
| $this->headers = $headers; | ||
| $this->amzHeaders = $amzHeaders; | ||
|
|
||
| $this->completeMultipartUpload($path, $uploadId, $metadata['parts']); | ||
| } | ||
|
|
||
| return $metadata['chunks']; | ||
| } | ||
|
|
@@ -307,7 +366,7 @@ protected function completeMultipartUpload(string $path, string $uploadId, array | |
| { | ||
| $uri = $path !== '' ? '/'.\str_replace(['%2F', '%3F'], ['/', '?'], \rawurlencode($path)) : '/'; | ||
|
|
||
| \ksort($parts); | ||
| \ksort($parts, SORT_NUMERIC); | ||
|
|
||
| $body = '<CompleteMultipartUpload>'; | ||
| foreach ($parts as $key => $etag) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uploadChunkself-callsprepareUpload, bypassingTelemetrywrapperLocal::uploadChunkcalls$this->prepareUpload(...)directly on theLocalinstance (line 78). When aTelemetrydevice wrapsLocaland a caller invokesTelemetry::uploadChunk, the delegatedLocal::uploadChunktriggersLocal::prepareUploadinternally — never going throughTelemetry::prepareUpload. That internal invocation is therefore not measured or recorded. Additionally, whenupload()is the caller (which already calledprepareUploadon the way in), the metadata is initialized twice, which works only because??=is idempotent.S3::uploadChunkhas no such internal call, making the two adapters asymmetric in their self-initialization behaviour.