diff --git a/src/Blob/BlobClient.php b/src/Blob/BlobClient.php index ff90d9a..eab0403 100644 --- a/src/Blob/BlobClient.php +++ b/src/Blob/BlobClient.php @@ -197,7 +197,11 @@ private function uploadInBlocks(StreamInterface $content, UploadBlobOptions $opt $pool->promise()->wait(); - $this->putBlockList($blocks, $options); + $this->putBlockList( + $blocks, + $options->contentType, + StreamUtils::hash($content, 'md5', true), + ); } private function putBlockAsync(Block $block, StreamInterface $content): PromiseInterface @@ -218,7 +222,7 @@ private function putBlockAsync(Block $block, StreamInterface $content): PromiseI /** * @param Block[] $blocks */ - private function putBlockList(array $blocks, UploadBlobOptions $options): void + private function putBlockList(array $blocks, ?string $contentType, string $contentMD5): void { try { $this->client->put($this->uri, [ @@ -226,7 +230,8 @@ private function putBlockList(array $blocks, UploadBlobOptions $options): void 'comp' => 'blocklist', ], 'headers' => [ - 'x-ms-blob-content-type' => $options->contentType, + 'x-ms-blob-content-type' => $contentType, + 'x-ms-blob-content-md5' => base64_encode($contentMD5), ], 'body' => (new PutBlockRequestBody($blocks))->toXml()->asXML(), ]); diff --git a/src/Blob/Exceptions/DateMalformedStringException.php b/src/Blob/Exceptions/DeserializationException.php similarity index 55% rename from src/Blob/Exceptions/DateMalformedStringException.php rename to src/Blob/Exceptions/DeserializationException.php index a38ebb0..88c2302 100644 --- a/src/Blob/Exceptions/DateMalformedStringException.php +++ b/src/Blob/Exceptions/DeserializationException.php @@ -4,4 +4,4 @@ namespace AzureOss\Storage\Blob\Exceptions; -final class DateMalformedStringException extends \Exception {} +final class DeserializationException extends \Exception {} diff --git a/src/Blob/Helpers/DateHelper.php b/src/Blob/Helpers/DateHelper.php index 8100e84..14a16b9 100644 --- a/src/Blob/Helpers/DateHelper.php +++ b/src/Blob/Helpers/DateHelper.php @@ -4,6 +4,8 @@ namespace AzureOss\Storage\Blob\Helpers; +use AzureOss\Storage\Blob\Exceptions\DeserializationException; + /** * @internal */ @@ -15,4 +17,14 @@ public static function formatAs8601Zulu(\DateTimeInterface $date): string ->setTimezone(new \DateTimeZone('UTC')) ->format('Y-m-d\TH:i:s\Z'); } + + public static function deserializeDateRfc1123Date(string $date): \DateTimeInterface + { + $result = \DateTimeImmutable::createFromFormat(\DateTimeInterface::RFC1123, $date); + if ($result === false) { + throw new DeserializationException("Azure returned a malformed date."); + } + + return $result; + } } diff --git a/src/Blob/Models/BlobContainerProperties.php b/src/Blob/Models/BlobContainerProperties.php index db995fb..e3fc561 100644 --- a/src/Blob/Models/BlobContainerProperties.php +++ b/src/Blob/Models/BlobContainerProperties.php @@ -4,7 +4,7 @@ namespace AzureOss\Storage\Blob\Models; -use AzureOss\Storage\Blob\Exceptions\DateMalformedStringException; +use AzureOss\Storage\Blob\Exceptions\DeserializationException; use AzureOss\Storage\Blob\Helpers\MetadataHelper; use Psr\Http\Message\ResponseInterface; @@ -22,7 +22,7 @@ public static function fromResponseHeaders(ResponseInterface $response): self { $lastModified = \DateTimeImmutable::createFromFormat(\DateTimeInterface::RFC1123, $response->getHeaderLine('Last-Modified')); if ($lastModified === false) { - throw new DateMalformedStringException("Azure returned a malformed date."); + throw new DeserializationException("Azure returned a malformed date."); } return new self($lastModified, MetadataHelper::headersToMetadata($response->getHeaders())); @@ -32,7 +32,7 @@ public static function fromXml(\SimpleXMLElement $xml): self { $lastModified = \DateTimeImmutable::createFromFormat(\DateTimeInterface::RFC1123, (string) $xml->{'Last-Modified'}); if ($lastModified === false) { - throw new DateMalformedStringException("Azure returned a malformed date."); + throw new DeserializationException("Azure returned a malformed date."); } return new self( diff --git a/src/Blob/Models/BlobProperties.php b/src/Blob/Models/BlobProperties.php index 1955e58..ac11e79 100644 --- a/src/Blob/Models/BlobProperties.php +++ b/src/Blob/Models/BlobProperties.php @@ -4,7 +4,7 @@ namespace AzureOss\Storage\Blob\Models; -use AzureOss\Storage\Blob\Exceptions\DateMalformedStringException; +use AzureOss\Storage\Blob\Helpers\DateHelper; use AzureOss\Storage\Blob\Helpers\MetadataHelper; use Psr\Http\Message\ResponseInterface; @@ -17,39 +17,39 @@ public function __construct( public readonly \DateTimeInterface $lastModified, public readonly int $contentLength, public readonly string $contentType, - public readonly string $contentMD5, + public readonly ?string $contentMD5, public readonly array $metadata, ) {} public static function fromResponseHeaders(ResponseInterface $response): self { - $lastModified = \DateTimeImmutable::createFromFormat(\DateTimeInterface::RFC1123, $response->getHeaderLine('Last-Modified')); - if ($lastModified === false) { - throw new DateMalformedStringException("Azure returned a malformed date."); - } - return new BlobProperties( - $lastModified, + DateHelper::deserializeDateRfc1123Date($response->getHeaderLine('Last-Modified')), (int) $response->getHeaderLine('Content-Length'), $response->getHeaderLine('Content-Type'), - $response->getHeaderLine('Content-MD5'), + self::deserializeContentMD5($response->getHeaderLine('Content-MD5')), MetadataHelper::headersToMetadata($response->getHeaders()), ); } public static function fromXml(\SimpleXMLElement $xml): self { - $lastModified = \DateTimeImmutable::createFromFormat(\DateTimeInterface::RFC1123, (string) $xml->{'Last-Modified'}); - if ($lastModified === false) { - throw new DateMalformedStringException("Azure returned a malformed date."); - } - return new self( - $lastModified, + DateHelper::deserializeDateRfc1123Date((string) $xml->{'Last-Modified'}), (int) $xml->{'Content-Length'}, (string) $xml->{'Content-Type'}, - (string) $xml->{'Content-MD5'}, + self::deserializeContentMD5((string) $xml->{'Content-MD5'}), [], ); } + + public static function deserializeContentMD5(string $contentMD5): ?string + { + $result = base64_decode($contentMD5, true); + if ($result === false) { + return null; + } + + return bin2hex($result); + } } diff --git a/src/Blob/Models/UploadBlobOptions.php b/src/Blob/Models/UploadBlobOptions.php index 797e1e9..955c9c6 100644 --- a/src/Blob/Models/UploadBlobOptions.php +++ b/src/Blob/Models/UploadBlobOptions.php @@ -7,7 +7,7 @@ final class UploadBlobOptions { /** - * @param int $initialTransferSize The size of the first range request in bytes. Blobs smaller than this limit will be downloaded in a single request. Blobs larger than this limit will continue being downloaded in chunks of size MaximumTransferSize. + * @param int $initialTransferSize The size of the first range request in bytes. Blobs smaller than this limit will be transferred in a single request. Blobs larger than this limit will continue being transferred in chunks of size MaximumTransferSize. * @param int $maximumTransferSize The maximum length of a transfer in bytes. * @param int $maximumConcurrency The maximum number of workers that may be used in a parallel transfer. */ diff --git a/tests/Blob/Feature/BlobClientTest.php b/tests/Blob/Feature/BlobClientTest.php index 9bc550d..d80d006 100644 --- a/tests/Blob/Feature/BlobClientTest.php +++ b/tests/Blob/Feature/BlobClientTest.php @@ -194,9 +194,10 @@ public function upload_works_with_parallel_upload(): void self::assertEquals("text/plain", $properties->contentType); self::assertEquals(1000, $properties->contentLength); - $afterUploadContent = $this->blobClient->downloadStreaming()->content; + $blob = $this->blobClient->downloadStreaming(); - self::assertEquals($beforeUploadContent, $afterUploadContent); + self::assertEquals($beforeUploadContent, $blob->content); + self::assertEquals(md5($beforeUploadContent), $blob->properties->contentMD5); }); }