├── .gitignore ├── LICENSE ├── composer.json ├── readme.md └── src ├── AzureBlobServiceProvider.php └── AzureFilesystem.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | bin 4 | coverage 5 | coverage.xml 6 | composer.lock 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ijin82/flysystem-azure", 3 | "description": "Laravel 10+ adapter for Windows Azure (Fork/Hack of league/flysystem-azure)", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Frank de Jonge", 8 | "email": "info@frenky.net" 9 | }, 10 | { 11 | "name": "Ilya Rogojin", 12 | "email": "ilya.rogojin@gmail.com" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=8.0", 17 | "league/flysystem": "^3.16.0", 18 | "league/flysystem-azure-blob-storage": "^3.28.0" 19 | }, 20 | "require-dev": { 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Ijin82\\Flysystem\\Azure\\": "src/" 25 | } 26 | }, 27 | "config": { 28 | "bin-dir": "bin" 29 | }, 30 | "extra": { 31 | "branch-alias": { 32 | "dev-master": "1.0-dev" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 2 | [![Total Downloads](https://poser.pugx.org/ijin82/flysystem-azure/downloads)](https://packagist.org/packages/ijin82/flysystem-azure) 3 | 4 | ### WARNING! 1.0.7.1 is the last version for Laravel 5+ and PHP7 5 | 6 | # Azure Blob custom filesystem for Laravel 10+ and PHP8 7 | This solution is mostly hack around [thephpleague/flysystem-azure-blob-storage](https://github.com/thephpleague/flysystem-azure-blob-storage) 8 | and requires 3+ version. 9 | Tested with Laravel 10, probably supported Laravel 8 & 9, additional tests needed. 10 | Check your solutions carefully before release. 11 | 12 | # Why forked? 13 | Needed to integrate with L5+ out of the box, and **url** method for **Storage** interface. 14 | 15 | # How to install in Laravel application 16 | Install package 17 | ```bash 18 | composer require ijin82/flysystem-azure 19 | ``` 20 | Open **config/app.php** and add this to providers section 21 | ``` 22 | Ijin82\Flysystem\Azure\AzureBlobServiceProvider::class, 23 | ``` 24 | 25 | Open **config/filesystems.php** and add this stuff to disks section 26 | ``` 27 | 'my_azure_disk1' => [ 28 | 'driver' => 'azure_blob', 29 | 'endpoint' => env('AZURE_BLOB_STORAGE_ENDPOINT'), 30 | 'container' => env('AZURE_BLOB_STORAGE_CONTAINER1'), 31 | 'blob_service_url' => env('AZURE_BLOB_SERVICE_URL'), 32 | ], 33 | ``` 34 | 35 | Open your **.env** and add variables for your disk 36 | ``` 37 | AZURE_BLOB_SERVICE_URL={your-blob-service-url} 38 | AZURE_BLOB_STORAGE_ENDPOINT="DefaultEndpointsProtocol=https;AccountName={your-account-name};AccountKey={your-account-key};" 39 | AZURE_BLOB_STORAGE_CONTAINER1={your-container-name} 40 | ``` 41 | 1. You can get **AZURE_BLOB_SERVICE_URL** variable from **Properties** section of your Storage account settings. 42 | That is an url named *PRIMARY BLOB SERVICE ENDPOINT* or *SECONDARY BLOB SERVICE ENDPOINT*. 43 | Same time that could be Azure CDN address (related to your endpoint) to use it as public address for files URL generation. 44 | 1. You can get **AZURE_BLOB_STORAGE_ENDPOINT** variable from **Access keys** section of your Storage account settings. 45 | That is named *CONNECTION STRING* 46 | 1. **AZURE_BLOB_STORAGE_CONTAINER1** is the name of your pre-created container, that you can add at **Overview** 47 | section of your Storage account settings. 48 | 49 | # Storage methods supported 50 | **REM** Path related to container, you need file prefix only if you need subfolder inside container 51 | ```php 52 | # Upload example 53 | Storage::disk('disk1')->put('file-folder/file1.png', 54 | file_get_contents('/my/file/path/file1.png'), 55 | [ 56 | 'mimetype' => 'image/png', 57 | ] 58 | ); 59 | ``` 60 | ```php 61 | # Get file URL example 62 | $publicUrl = Storage::disk('disk1')->url('file-folder/file1.png'); 63 | ``` 64 | ```php 65 | # Check file exists example 66 | $exists1 = Storage::disk('disk1')->exists('file-folder/file1.png'); 67 | ``` 68 | ```php 69 | # Get file contents example 70 | $contents = Storage::disk('disk1')->get('file-folder/file1.png'); 71 | ``` 72 | ```php 73 | # Delete file example 74 | Storage::disk('disk1')->delete('file-folder/file1.png'); 75 | ``` 76 | ```php 77 | # Delete directory example 78 | # Warning, recursive folder deletion! 79 | Storage::disk('disk1')->deleteDir('file-folder'); 80 | ``` 81 | ```php 82 | # Put uploaded file to storage example 83 | # $file could be file path on disk (string) OR type of File|UploadedFile 84 | Storage::disk('disk1')->putFileAs('file-folder', $file, 'file1.png'); 85 | ``` 86 | 87 | # How to upload file 88 | ```php 89 | public function someUploadFuncName(Request $request) 90 | { 91 | $file = $request->file('file_name_from_request'); 92 | 93 | // .. file name logic 94 | // .. file folder logic 95 | 96 | $file->storeAs($fileFolder, $fileName, [ 97 | 'disk' => 'my_azure_disk1' 98 | ]); 99 | 100 | // save file name logic 101 | // to create file URL by name later 102 | // maybe you want to save file name and folder separated 103 | $fileNameToSave = $folderName . '/' . $diskFileName; 104 | 105 | // .. save file name to DB or etc. 106 | } 107 | ``` 108 | 109 | # How to get file URL 110 | 111 | We got file name for selected disk (folder related if folder exists) 112 | ```php 113 | echo Storage::disk('my_azure_disk1')->url($fileName); 114 | ``` 115 | That is also working in blade templates like this 116 | ``` 117 | {{ $fileName }} 119 | ``` 120 | 121 | # How to delete file 122 | ```php 123 | public function someDeleteFuncName($id) 124 | { 125 | $file = SomeFileModel::findOrFail($id); 126 | Storage::disk('my_azure_disk1')->delete($file->name); 127 | $file->delete(); 128 | 129 | // go back or etc.. 130 | } 131 | ``` 132 | # Mimetypes (this can be useful) 133 | Sometimes you need to set up mime types manually (for CDN maybe) to get back correct mime type values. You can do that like this (couple types forced for example): 134 | ```php 135 | $fileConents = Storage::disk('public_or_another_local_disk')->get($file); 136 | 137 | $forcedMimes = [ 138 | 'js' => 'application/javascript', 139 | 'json' => 'application/json', 140 | ]; 141 | 142 | $fileExt = \File::extension($file); 143 | 144 | if (array_key_exists($fileExt, $forcedMimes)) { 145 | $fileMime = $forcedMimes[$fileExt]; 146 | } else { 147 | $fileMime = mime_content_type(Storage::disk('public_or_another_local_disk')->path($file)); 148 | } 149 | 150 | Storage::disk('my_custom_azure_disk')->put($fileName, $fileConents, [ 151 | 'mimetype' => $fileMime, 152 | ]); 153 | ``` 154 | You can use wget to get response with headers including *Content-Type* 155 | ``` 156 | wget -S https://your-file-host.com/file-name.jpg 157 | ``` 158 | 159 | # Additions 160 | 1. Original repo is [here](https://github.com/thephpleague/flysystem-azure-blob-storage) 161 | 2. [How to use blob storage from PHP](https://docs.microsoft.com/en-us/azure/storage/storage-php-how-to-use-blobs) 162 | 4. Feel free to send pull requests and issues. 163 | -------------------------------------------------------------------------------- /src/AzureBlobServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'public', 35 | 'public_url' => ($config['blob_service_url'] . '/' . $config['container']), 36 | 'container' => $config['container'] 37 | ] 38 | ); 39 | 40 | return $filesystem; 41 | }); 42 | } 43 | 44 | /** 45 | * Register bindings in the container. 46 | * 47 | * @return void 48 | */ 49 | public function register() 50 | { 51 | // 52 | } 53 | } -------------------------------------------------------------------------------- /src/AzureFilesystem.php: -------------------------------------------------------------------------------- 1 | config = new Config($config); 47 | $this->pathNormalizer = $pathNormalizer ?? new WhitespacePathNormalizer(); 48 | } 49 | 50 | public function url(string $path): string 51 | { 52 | return $this->publicUrl($path, []); 53 | } 54 | 55 | public function putFileAs($path, $file, $name = null, $options = []) 56 | { 57 | $contents = ((is_string($file) && file_exists($file)) ? file_get_contents($file) : $file->get()); 58 | 59 | $config = $this->config->extend($options); 60 | 61 | $this->adapter->write( 62 | $path . '/' . $name, 63 | $contents, 64 | $config 65 | ); 66 | } 67 | 68 | public function put($path, $contents, $options = []) 69 | { 70 | /** 71 | * todo: set up mimetype from options 72 | * Storage::disk('account_fonts')->put('font-' . $font->id . '.css', $content, [ 73 | * 'mimetype' => ' text/css', 74 | * ]); 75 | */ 76 | 77 | $config = $this->config->extend($options); 78 | 79 | return $this->adapter->write( 80 | $path, 81 | $contents, 82 | $config 83 | ); 84 | } 85 | 86 | public function delete(string $location): void 87 | { 88 | $this->adapter->delete($location); 89 | } 90 | 91 | public function deleteDir($folder) 92 | { 93 | return $this->adapter->deleteDirectory($folder); 94 | } 95 | 96 | public function exists(string $location): bool 97 | { 98 | return $this->fileExists($location); 99 | } 100 | 101 | public function get(string $location): string 102 | { 103 | return $this->read($location); 104 | } 105 | 106 | // original Filesystem src 107 | 108 | public function fileExists(string $location): bool 109 | { 110 | return $this->adapter->fileExists($this->pathNormalizer->normalizePath($location)); 111 | } 112 | 113 | public function directoryExists(string $location): bool 114 | { 115 | return $this->adapter->directoryExists($this->pathNormalizer->normalizePath($location)); 116 | } 117 | 118 | public function has(string $location): bool 119 | { 120 | $path = $this->pathNormalizer->normalizePath($location); 121 | 122 | return $this->adapter->fileExists($path) || $this->adapter->directoryExists($path); 123 | } 124 | 125 | public function write(string $location, string $contents, array $config = []): void 126 | { 127 | $this->adapter->write( 128 | $this->pathNormalizer->normalizePath($location), 129 | $contents, 130 | $this->config->extend($config) 131 | ); 132 | } 133 | 134 | public function writeStream(string $location, $contents, array $config = []): void 135 | { 136 | /* @var resource $contents */ 137 | $this->assertIsResource($contents); 138 | $this->rewindStream($contents); 139 | $this->adapter->writeStream( 140 | $this->pathNormalizer->normalizePath($location), 141 | $contents, 142 | $this->config->extend($config) 143 | ); 144 | } 145 | 146 | public function read(string $location): string 147 | { 148 | return $this->adapter->read($this->pathNormalizer->normalizePath($location)); 149 | } 150 | 151 | public function readStream(string $location) 152 | { 153 | return $this->adapter->readStream($this->pathNormalizer->normalizePath($location)); 154 | } 155 | 156 | // redeclared 157 | // public function delete(string $location): void 158 | // { 159 | // $this->adapter->delete($this->pathNormalizer->normalizePath($location)); 160 | // } 161 | 162 | public function deleteDirectory(string $location): void 163 | { 164 | $this->adapter->deleteDirectory($this->pathNormalizer->normalizePath($location)); 165 | } 166 | 167 | public function createDirectory(string $location, array $config = []): void 168 | { 169 | $this->adapter->createDirectory( 170 | $this->pathNormalizer->normalizePath($location), 171 | $this->config->extend($config) 172 | ); 173 | } 174 | 175 | public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing 176 | { 177 | $path = $this->pathNormalizer->normalizePath($location); 178 | $listing = $this->adapter->listContents($path, $deep); 179 | 180 | return new DirectoryListing($this->pipeListing($location, $deep, $listing)); 181 | } 182 | 183 | private function pipeListing(string $location, bool $deep, iterable $listing): Generator 184 | { 185 | try { 186 | foreach ($listing as $item) { 187 | yield $item; 188 | } 189 | } catch (Throwable $exception) { 190 | throw UnableToListContents::atLocation($location, $deep, $exception); 191 | } 192 | } 193 | 194 | public function move(string $source, string $destination, array $config = []): void 195 | { 196 | $config = $this->resolveConfigForMoveAndCopy($config); 197 | $from = $this->pathNormalizer->normalizePath($source); 198 | $to = $this->pathNormalizer->normalizePath($destination); 199 | 200 | if ($from === $to) { 201 | $resolutionStrategy = $config->get(Config::OPTION_MOVE_IDENTICAL_PATH, ResolveIdenticalPathConflict::TRY); 202 | 203 | if ($resolutionStrategy === ResolveIdenticalPathConflict::FAIL) { 204 | throw UnableToMoveFile::sourceAndDestinationAreTheSame($source, $destination); 205 | } elseif ($resolutionStrategy === ResolveIdenticalPathConflict::IGNORE) { 206 | return; 207 | } 208 | } 209 | 210 | $this->adapter->move($from, $to, $config); 211 | } 212 | 213 | public function copy(string $source, string $destination, array $config = []): void 214 | { 215 | $config = $this->resolveConfigForMoveAndCopy($config); 216 | $from = $this->pathNormalizer->normalizePath($source); 217 | $to = $this->pathNormalizer->normalizePath($destination); 218 | 219 | if ($from === $to) { 220 | $resolutionStrategy = $config->get(Config::OPTION_COPY_IDENTICAL_PATH, ResolveIdenticalPathConflict::TRY); 221 | 222 | if ($resolutionStrategy === ResolveIdenticalPathConflict::FAIL) { 223 | throw UnableToCopyFile::sourceAndDestinationAreTheSame($source, $destination); 224 | } elseif ($resolutionStrategy === ResolveIdenticalPathConflict::IGNORE) { 225 | return; 226 | } 227 | } 228 | 229 | $this->adapter->copy($from, $to, $config); 230 | } 231 | 232 | public function lastModified(string $path): int 233 | { 234 | return $this->adapter->lastModified($this->pathNormalizer->normalizePath($path))->lastModified(); 235 | } 236 | 237 | public function fileSize(string $path): int 238 | { 239 | return $this->adapter->fileSize($this->pathNormalizer->normalizePath($path))->fileSize(); 240 | } 241 | 242 | public function mimeType(string $path): string 243 | { 244 | return $this->adapter->mimeType($this->pathNormalizer->normalizePath($path))->mimeType(); 245 | } 246 | 247 | public function setVisibility(string $path, string $visibility): void 248 | { 249 | $this->adapter->setVisibility($this->pathNormalizer->normalizePath($path), $visibility); 250 | } 251 | 252 | public function visibility(string $path): string 253 | { 254 | return $this->adapter->visibility($this->pathNormalizer->normalizePath($path))->visibility(); 255 | } 256 | 257 | public function publicUrl(string $path, array $config = []): string 258 | { 259 | $this->publicUrlGenerator ??= $this->resolvePublicUrlGenerator() 260 | ?? throw UnableToGeneratePublicUrl::noGeneratorConfigured($path); 261 | $config = $this->config->extend($config); 262 | 263 | return $this->publicUrlGenerator->publicUrl( 264 | $this->pathNormalizer->normalizePath($path), 265 | $config, 266 | ); 267 | } 268 | 269 | public function temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []): string 270 | { 271 | $generator = $this->temporaryUrlGenerator ?? $this->adapter; 272 | 273 | if ($generator instanceof TemporaryUrlGenerator) { 274 | return $generator->temporaryUrl( 275 | $this->pathNormalizer->normalizePath($path), 276 | $expiresAt, 277 | $this->config->extend($config) 278 | ); 279 | } 280 | 281 | throw UnableToGenerateTemporaryUrl::noGeneratorConfigured($path); 282 | } 283 | 284 | public function checksum(string $path, array $config = []): string 285 | { 286 | $config = $this->config->extend($config); 287 | 288 | if (!$this->adapter instanceof ChecksumProvider) { 289 | return $this->calculateChecksumFromStream($path, $config); 290 | } 291 | 292 | try { 293 | return $this->adapter->checksum( 294 | $this->pathNormalizer->normalizePath($path), 295 | $config, 296 | ); 297 | } catch (ChecksumAlgoIsNotSupported) { 298 | return $this->calculateChecksumFromStream( 299 | $this->pathNormalizer->normalizePath($path), 300 | $config, 301 | ); 302 | } 303 | } 304 | 305 | private function resolvePublicUrlGenerator(): ?PublicUrlGenerator 306 | { 307 | if ($publicUrl = $this->config->get('public_url')) { 308 | return match (true) { 309 | is_array($publicUrl) => new ShardedPrefixPublicUrlGenerator($publicUrl), 310 | default => new PrefixPublicUrlGenerator($publicUrl), 311 | }; 312 | } 313 | 314 | if ($this->adapter instanceof PublicUrlGenerator) { 315 | return $this->adapter; 316 | } 317 | 318 | return null; 319 | } 320 | 321 | /** 322 | * @param mixed $contents 323 | */ 324 | private function assertIsResource($contents): void 325 | { 326 | if (is_resource($contents) === false) { 327 | throw new InvalidStreamProvided( 328 | "Invalid stream provided, expected stream resource, received " . gettype($contents) 329 | ); 330 | } elseif ($type = get_resource_type($contents) !== 'stream') { 331 | throw new InvalidStreamProvided( 332 | "Invalid stream provided, expected stream resource, received resource of type " . $type 333 | ); 334 | } 335 | } 336 | 337 | /** 338 | * @param resource $resource 339 | */ 340 | private function rewindStream($resource): void 341 | { 342 | if (ftell($resource) !== 0 && stream_get_meta_data($resource)['seekable']) { 343 | rewind($resource); 344 | } 345 | } 346 | 347 | private function resolveConfigForMoveAndCopy(array $config): Config 348 | { 349 | $retainVisibility = $this->config->get(Config::OPTION_RETAIN_VISIBILITY, $config[Config::OPTION_RETAIN_VISIBILITY] ?? true); 350 | $fullConfig = $this->config->extend($config); 351 | 352 | /* 353 | * By default, we retain visibility. When we do not retain visibility, the visibility setting 354 | * from the default configuration is ignored. Only when it is set explicitly, we propagate the 355 | * setting. 356 | */ 357 | if ($retainVisibility && !array_key_exists(Config::OPTION_VISIBILITY, $config)) { 358 | $fullConfig = $fullConfig->withoutSettings(Config::OPTION_VISIBILITY)->extend($config); 359 | } 360 | 361 | return $fullConfig; 362 | } 363 | } 364 | --------------------------------------------------------------------------------