diff --git a/README.md b/README.md index 5164143..6d857a6 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,40 @@ $adapter = new AzureBlobStorageAdapter( ); ``` +### Upload transfer tuning + +When writing files, you can control the upload behavior via Flysystem's Config on write/writeStream calls: + +- initialTransferSize: int (bytes) — size threshold for first transfer; smaller blobs are uploaded in a single request; larger ones switch to chunked upload. +- maximumTransferSize: int (bytes) — chunk size for block uploads. +- maximumConcurrency: int — number of concurrent workers for parallel uploads. + +Example: + +```php +use League\Flysystem\Config; + +$filesystem->write('path/to/file.txt', $contents, new Config([ + 'initialTransferSize' => 64 * 1024 * 1024, // 64MB + 'maximumTransferSize' => 8 * 1024 * 1024, // 8MB + 'maximumConcurrency' => 8, +])); +``` + +### HTTP headers + +```php +$filesystem->write('path/to/file.txt', $contents, new Config([ + 'httpHeaders' => [ + 'cacheControl' => 'public, max-age=31536000', + 'contentDisposition' => 'inline', + 'contentEncoding' => 'gzip', + 'contentLanguage' => 'en', + 'contentType' => 'text/plain', + ], +])); +``` + Note that for direct public URLs to work, your container must be configured with public access. If your container is private, you should use the default SAS token approach. ## Documentation diff --git a/src/AzureBlobStorageAdapter.php b/src/AzureBlobStorageAdapter.php index df081b4..25edee4 100644 --- a/src/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorageAdapter.php @@ -32,6 +32,7 @@ use League\Flysystem\UrlGeneration\TemporaryUrlGenerator; use League\MimeTypeDetection\FinfoMimeTypeDetector; use League\MimeTypeDetection\MimeTypeDetector; +use AzureOss\FlysystemAzureBlobStorage\Support\ConfigArrayParser; final class AzureBlobStorageAdapter implements FilesystemAdapter, ChecksumProvider, TemporaryUrlGenerator, PublicUrlGenerator { @@ -85,35 +86,96 @@ public function directoryExists(string $path): bool public function write(string $path, string $contents, Config $config): void { - $this->upload($path, $contents); + $this->upload($path, $contents, $config); } public function writeStream(string $path, $contents, Config $config): void { - $this->upload($path, $contents); + $this->upload($path, $contents, $config); } /** * @param string|resource $contents */ - private function upload(string $path, $contents): void + private function upload(string $path, $contents, ?Config $config = null): void { try { - $path = $this->prefixer->prefixPath($path); - $mimetype = $this->mimeTypeDetector->detectMimetype($path, $contents); + $options = $this->buildUploadOptionsFromConfig($config); - $options = new UploadBlobOptions( - contentType: $mimetype, - ); + if ($options->httpHeaders->contentType === "" && is_string($contents)) { + $options->httpHeaders->contentType = $this->mimeTypeDetector->detectMimeTypeFromBuffer($contents) ?? ""; + } $this->containerClient - ->getBlobClient($path) + ->getBlobClient($this->prefixer->prefixPath($path)) ->upload($contents, $options); } catch (\Throwable $e) { throw UnableToWriteFile::atLocation($path, previous: $e); } } + private function buildUploadOptionsFromConfig(?Config $config): UploadBlobOptions + { + $options = new UploadBlobOptions(); + + if ($config === null) { + return $options; + } + + $data = $config->toArray(); + + $initialTransferSize = ConfigArrayParser::parseIntFromArray($data, 'initialTransferSize'); + if ($initialTransferSize !== null) { + $options->initialTransferSize = $initialTransferSize; + } + + $maximumTransferSize = ConfigArrayParser::parseIntFromArray($data, 'maximumTransferSize'); + if ($maximumTransferSize !== null) { + $options->maximumTransferSize = $maximumTransferSize; + } + + $maximumConcurrency = ConfigArrayParser::parseIntFromArray($data, 'maximumConcurrency'); + if ($maximumConcurrency !== null) { + $options->maximumConcurrency = $maximumConcurrency; + } + + $headers = ConfigArrayParser::parseArrayFromArray($data, 'httpHeaders'); + if ($headers !== null) { + $cacheControl = ConfigArrayParser::parseStringFromArray($headers, 'cacheControl', 'httpHeaders.'); + if ($cacheControl !== null) { + $options->httpHeaders->cacheControl = $cacheControl; + } + + $contentDisposition = ConfigArrayParser::parseStringFromArray($headers, 'contentDisposition', 'httpHeaders.'); + if ($contentDisposition !== null) { + $options->httpHeaders->contentDisposition = $contentDisposition; + } + + $contentEncoding = ConfigArrayParser::parseStringFromArray($headers, 'contentEncoding', 'httpHeaders.'); + if ($contentEncoding !== null) { + $options->httpHeaders->contentEncoding = $contentEncoding; + } + + $contentHash = ConfigArrayParser::parseStringFromArray($headers, 'contentHash', 'httpHeaders.'); + if ($contentHash !== null) { + $options->httpHeaders->contentHash = $contentHash; + } + + $contentLanguage = ConfigArrayParser::parseStringFromArray($headers, 'contentLanguage', 'httpHeaders.'); + if ($contentLanguage !== null) { + $options->httpHeaders->contentLanguage = $contentLanguage; + } + + $contentType = ConfigArrayParser::parseStringFromArray($headers, 'contentType', 'httpHeaders.'); + if ($contentType !== null) { + $options->httpHeaders->contentType = $contentType; + } + } + + return $options; + } + + public function read(string $path): string { try { diff --git a/src/Support/ConfigArrayParser.php b/src/Support/ConfigArrayParser.php new file mode 100644 index 0000000..c795dd1 --- /dev/null +++ b/src/Support/ConfigArrayParser.php @@ -0,0 +1,54 @@ + $data + */ + public static function parseIntFromArray(array $data, string $key): ?int + { + if (!array_key_exists($key, $data) || $data[$key] === null) { + return null; + } + if (!is_int($data[$key])) { + throw new \RuntimeException(sprintf('%s must be an int.', $key)); + } + return $data[$key]; + } + + /** + * @param array $data + * @return array|null + */ + public static function parseArrayFromArray(array $data, string $key): ?array + { + $value = $data[$key] ?? null; + if ($value === null) { + return null; + } + if (!is_array($value)) { + throw new \RuntimeException(sprintf('%s must be an array.', $key)); + } + return $value; + } + + /** + * @param array $data + */ + public static function parseStringFromArray(array $data, string $key, string $contextPrefix = ''): ?string + { + $value = $data[$key] ?? null; + if ($value === null) { + return null; + } + if (!is_string($value)) { + $fullKey = $contextPrefix !== '' ? $contextPrefix . $key : $key; + throw new \RuntimeException(sprintf('%s must be a string.', $fullKey)); + } + return $value; + } +}