├── .github
└── workflows
│ └── php.yml
├── .gitignore
├── LICENSE
├── composer.json
├── phpunit.xml
├── readme.md
├── src
├── BunnyCDNAdapter.php
├── BunnyCDNClient.php
├── BunnyCDNRegion.php
├── Exceptions
│ ├── BunnyCDNException.php
│ └── NotFoundException.php
├── Util.php
└── WriteBatchFile.php
└── tests
├── ClientDI_Example.php
├── ClientTest.php
├── FlysystemAdapterTest.php
├── MockClient.php
├── PrefixTest.php
└── UtilityClassTest.php
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches: [ master, v2, v3 ]
6 | pull_request:
7 | branches: [ master, v2, v3 ]
8 |
9 | jobs:
10 | build:
11 | runs-on: 'ubuntu-latest'
12 | strategy:
13 | matrix:
14 | php-versions: [ '8.0', '8.1', '8.2' ]
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 |
19 | - name: Setup PHP with PCOV
20 | uses: shivammathur/setup-php@v2
21 | with:
22 | php-version: ${{ matrix.php-versions }}
23 | coverage: pcov
24 |
25 | - name: Validate composer.json and composer.lock
26 | run: composer validate --strict
27 |
28 | - name: Cache Composer packages
29 | id: composer-cache
30 | uses: actions/cache@v2
31 | with:
32 | path: vendor
33 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
34 | restore-keys: |
35 | ${{ runner.os }}-php-
36 |
37 | - name: Install dependencies
38 | run: composer install --prefer-dist --no-progress --no-interaction
39 |
40 | - name: Run linter
41 | run: ./vendor/bin/pint -v --test
42 |
43 | - name: Run tests
44 | run: vendor/bin/phpunit -c ./phpunit.xml
45 |
46 | - uses: php-actions/phpstan@v3
47 | with:
48 | path: src/
49 |
50 | - name: Upload to PCOV
51 | run: bash <(curl -s https://codecov.io/bash)
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | .phpunit.cache/
4 | .phpunit.result.cache
5 | bin
6 | clover.xml
7 | composer.lock
8 | coverage
9 | coverage.xml
10 | report/
11 | tests/ClientDI.php
12 | tests/files
13 | vendor
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Platform
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": "platformcommunity/flysystem-bunnycdn",
3 | "description": "Flysystem adapter for BunnyCDN",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "Alex",
8 | "email": "alex@platformapp.io"
9 | }
10 | ],
11 | "require": {
12 | "league/flysystem": "^3.16",
13 | "ext-json": "*",
14 | "guzzlehttp/guzzle": "^7.4",
15 | "league/mime-type-detection": "^1.11"
16 | },
17 | "require-dev": {
18 | "phpunit/phpunit": "@stable",
19 | "league/flysystem-adapter-test-utilities": "^3",
20 | "league/flysystem-memory": "^3.0",
21 | "league/flysystem-path-prefixing": "^3.3",
22 | "fzaninotto/faker": "^1.5",
23 | "laravel/pint": "^0.2.3"
24 | },
25 | "autoload-dev": {
26 | "psr-4": {
27 | "PlatformCommunity\\Flysystem\\BunnyCDN\\Tests\\": "tests/"
28 | }
29 | },
30 | "autoload": {
31 | "psr-4": {
32 | "PlatformCommunity\\Flysystem\\BunnyCDN\\Tests\\": "tests/",
33 | "PlatformCommunity\\Flysystem\\BunnyCDN\\": "src/"
34 | }
35 | },
36 | "extra": {
37 | "branch-alias": {
38 | "dev-master": "3.x-dev"
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ./tests/
21 |
22 |
23 |
24 |
25 |
26 | ./src/
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Flysystem Adapter for BunnyCDN Storage
4 | [](https://github.com/PlatformCommunity/flysystem-bunnycdn/actions) [](https://github.com/PlatformCommunity/flysystem-bunnycdn/actions)
[](https://codecov.io/gh/PlatformCommunity/flysystem-bunnycdn) [](https://packagist.org/packages/platformcommunity/flysystem-bunnycdn)  [](https://github.com/PlatformCommunity/flysystem-bunnycdn/blob/master/LICENSE) [](https://packagist.org/packages/platformcommunity/flysystem-bunnycdn)
5 |
6 |
7 | ## Installation
8 |
9 | To install `flysystem-bunnycdn`, require the package with no version constraint. This should match the `flysystem-bunnycdn` version with your version of FlySystem (v2, v3 etc).
10 |
11 | ```bash
12 | composer require platformcommunity/flysystem-bunnycdn "*"
13 | ```
14 |
15 | ## Usage
16 |
17 | ```php
18 | use League\Flysystem\Filesystem;
19 | use PlatformCommunity\Flysystem\BunnyCDN\BunnyCDNAdapter;
20 | use PlatformCommunity\Flysystem\BunnyCDN\BunnyCDNClient;
21 | use PlatformCommunity\Flysystem\BunnyCDN\BunnyCDNRegion;
22 |
23 | $adapter = new BunnyCDNAdapter(
24 | new BunnyCDNClient(
25 | 'storage-zone',
26 | 'api-key',
27 | BunnyCDNRegion::FALKENSTEIN
28 | )
29 | );
30 |
31 | $filesystem = new Filesystem($adapter);
32 | ```
33 |
34 | ### Usage with Pull Zones
35 |
36 | To have BunnyCDN adapter publish to a public CDN location, you have to a "Pull Zone" connected to your BunnyCDN Storage Zone. Add the full URL prefix of your Pull Zone (including `http://`/`https://`) to the BunnyCDNAdapter parameter like shown below.
37 |
38 |
39 | ```php
40 | $adapter = new BunnyCDNAdapter(
41 | new BunnyCDNClient(
42 | 'storage-zone',
43 | 'api-key',
44 | BunnyCDNRegion::FALKENSTEIN
45 | ),
46 | 'https://testing.b-cdn.net/' # Pull Zone URL
47 | );
48 | $filesystem = new Filesystem($adapter);
49 | ```
50 |
51 | _Note: You can also use your own domain name if it's configured in the pull zone._
52 |
53 | Once you add your pull zone, you can use the `->getUrl($path)`, or in Laravel, the `->url($path)` command to get the fully qualified public URL of your BunnyCDN assets.
54 |
55 | ## Usage in Laravel 9
56 | To add BunnyCDN adapter as a custom storage adapter in Laravel 9, install using the `v3` composer installer.
57 |
58 | ```bash
59 | composer require platformcommunity/flysystem-bunnycdn "^3.0"
60 | ```
61 |
62 | Next, install the adapter to your `AppServiceProvider` to give Laravel's FileSystem knowledge of the BunnyCDN adapter.
63 |
64 | ```php
65 | /**
66 | * Bootstrap any application services.
67 | *
68 | * @return void
69 | */
70 | public function boot()
71 | {
72 | Storage::extend('bunnycdn', function ($app, $config) {
73 | $adapter = new BunnyCDNAdapter(
74 | new BunnyCDNClient(
75 | $config['storage_zone'],
76 | $config['api_key'],
77 | $config['region']
78 | ),
79 | $config['pull_zone']
80 | );
81 |
82 | return new FilesystemAdapter(
83 | new Filesystem($adapter, $config),
84 | $adapter,
85 | $config
86 | );
87 | });
88 | }
89 | ```
90 |
91 | Finally, add the `bunnycdn` driver into your `config/filesystems.php` configuration file.
92 |
93 | ```php
94 | ...
95 |
96 | 'bunnycdn' => [
97 | 'driver' => 'bunnycdn',
98 | 'storage_zone' => env('BUNNYCDN_STORAGE_ZONE'),
99 | 'pull_zone' => env('BUNNYCDN_PULL_ZONE'),
100 | 'api_key' => env('BUNNYCDN_API_KEY'),
101 | 'region' => env('BUNNYCDN_REGION', \PlatformCommunity\Flysystem\BunnyCDN\BunnyCDNRegion::DEFAULT)
102 | ],
103 |
104 | ...
105 | ```
106 |
107 | Lastly, populate your `BUNNYCDN_STORAGE_ZONE`, `BUNNYCDN_API_KEY` `BUNNYCDN_REGION` variables in your `.env` file.
108 |
109 | ```dotenv
110 | BUNNYCDN_STORAGE_ZONE=testing_storage_zone
111 | BUNNYCDN_PULL_ZONE=https://testing.b-cdn.net
112 | BUNNYCDN_API_KEY="api-key"
113 | # BUNNYCDN_REGION=uk
114 | ```
115 |
116 | After that, you can use the `bunnycdn` disk in Laravel 9.
117 |
118 | ```php
119 | Storage::disk('bunnycdn')->put('index.html', 'Hello World');
120 |
121 | return response(Storage::disk('bunnycdn')->get('index.html'));
122 | ```
123 |
124 | _Note: You may have to run `php artisan config:clear` in order for your configuration to be refreshed if your app is running with a config cache driver / production mode._
125 |
126 | ## Regions
127 |
128 | For a full region list, please visit the [BunnyCDN API documentation page](https://docs.bunny.net/reference/storage-api#storage-endpoints).
129 |
130 | `flysystem-bunnycdn` also comes with constants for each region located within `PlatformCommunity\Flysystem\BunnyCDN\BunnyCDNRegion`.
131 |
132 | ### List of Regions
133 |
134 | ```php
135 | # Europe
136 | BunnyCDNRegion::FALKENSTEIN = 'de';
137 | BunnyCDNRegion::STOCKHOLM = 'se';
138 |
139 | # United Kingdom
140 | BunnyCDNRegion::UNITED_KINGDOM = 'uk';
141 |
142 | # USA
143 | BunnyCDNRegion::NEW_YORK = 'ny';
144 | BunnyCDNRegion::LOS_ANGELAS = 'la';
145 |
146 | # SEA
147 | BunnyCDNRegion::SINGAPORE = 'sg';
148 |
149 | # Oceania
150 | BunnyCDNRegion::SYDNEY = 'syd';
151 |
152 | # Africa
153 | BunnyCDNRegion::JOHANNESBURG = 'jh';
154 |
155 | # South America
156 | BunnyCDNRegion::BRAZIL = 'br';
157 | ```
158 |
159 | ## Contributing
160 |
161 | Pull requests welcome. Please feel free to lodge any issues as discussion points.
162 |
163 | ## Development
164 |
165 | Most of the understanding of how the Flysystem Adapter for BunnyCDN works comes from `tests/`. If you want to complete tests against a live BunnyCDN endpoint, copy the `tests/ClientDI_Example.php` to `tests/ClientDI.php` and insert your credentials into there. You can then run the whole suite by running `vendor/bin/phpunit`, or against a specific file by running `vendor/bin/phpunit --bootstrap tests/ClientDI.php tests/ClientTest.php`.
166 |
167 |
168 | ## Licence
169 |
170 | The Flysystem adapter for Bunny.net is licensed under [MIT](https://github.com/PlatformCommunity/flysystem-bunnycdn/blob/master/LICENSE).
171 |
--------------------------------------------------------------------------------
/src/BunnyCDNAdapter.php:
--------------------------------------------------------------------------------
1 | 2 && (string) \func_get_arg(2) !== '') {
43 | throw new \RuntimeException('PrefixPath is no longer supported directly. Use PathPrefixedAdapter instead: https://flysystem.thephpleague.com/docs/adapter/path-prefixing/');
44 | }
45 | }
46 |
47 | /**
48 | * @param $source
49 | * @param $destination
50 | * @param Config $config
51 | * @return void
52 | */
53 | public function copy($source, $destination, Config $config): void
54 | {
55 | try {
56 | $sourceLength = \strlen($source);
57 |
58 | foreach ($this->getFiles($source) as $file) {
59 | $this->copyFile($file, $destination.\substr($file, $sourceLength), $config);
60 | }
61 | } catch (UnableToReadFile|UnableToWriteFile $exception) {
62 | throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);
63 | }
64 | }
65 |
66 | /**
67 | * @param $path
68 | * @param $contents
69 | * @param Config $config
70 | */
71 | public function write($path, $contents, Config $config): void
72 | {
73 | try {
74 | $this->client->upload($path, $contents);
75 | // @codeCoverageIgnoreStart
76 | } catch (Exceptions\BunnyCDNException $e) {
77 | throw UnableToWriteFile::atLocation($path, $e->getMessage());
78 | }
79 | // @codeCoverageIgnoreEnd
80 | }
81 |
82 | /**
83 | * @param $path
84 | * @return string
85 | */
86 | public function read($path): string
87 | {
88 | try {
89 | return $this->client->download($path);
90 | // @codeCoverageIgnoreStart
91 | } catch (Exceptions\BunnyCDNException $e) {
92 | throw UnableToReadFile::fromLocation($path, $e->getMessage());
93 | }
94 | // @codeCoverageIgnoreEnd
95 | }
96 |
97 | /**
98 | * @param string $path
99 | * @param bool $deep
100 | * @return iterable
101 | */
102 | public function listContents(string $path, bool $deep): iterable
103 | {
104 | try {
105 | $entries = $this->client->list($path);
106 | // @codeCoverageIgnoreStart
107 | } catch (Exceptions\BunnyCDNException $e) {
108 | throw UnableToRetrieveMetadata::create($path, 'folder', $e->getMessage());
109 | }
110 | // @codeCoverageIgnoreEnd
111 |
112 | foreach ($entries as $item) {
113 | $content = $this->normalizeObject($item);
114 | yield $content;
115 |
116 | if ($deep && $content instanceof DirectoryAttributes) {
117 | foreach ($this->listContents($content->path(), $deep) as $deepItem) {
118 | yield $deepItem;
119 | }
120 | }
121 | }
122 | }
123 |
124 | /**
125 | * @param array $bunny_file_array
126 | * @return StorageAttributes
127 | */
128 | protected function normalizeObject(array $bunny_file_array): StorageAttributes
129 | {
130 | $normalised_path = Util::normalizePath(
131 | Util::replaceFirst(
132 | $bunny_file_array['StorageZoneName'].'/',
133 | '/',
134 | $bunny_file_array['Path'].$bunny_file_array['ObjectName']
135 | )
136 | );
137 |
138 | return match ($bunny_file_array['IsDirectory']) {
139 | true => new DirectoryAttributes(
140 | $normalised_path
141 | ),
142 | false => new FileAttributes(
143 | $normalised_path,
144 | $bunny_file_array['Length'],
145 | Visibility::PUBLIC,
146 | self::parse_bunny_timestamp($bunny_file_array['LastChanged']),
147 | $bunny_file_array['ContentType'] ?: $this->detectMimeType($bunny_file_array['Path'].$bunny_file_array['ObjectName']),
148 | $this->extractExtraMetadata($bunny_file_array)
149 | )
150 | };
151 | }
152 |
153 | /**
154 | * @param array $bunny_file_array
155 | * @return array
156 | */
157 | private function extractExtraMetadata(array $bunny_file_array): array
158 | {
159 | return [
160 | 'type' => $bunny_file_array['IsDirectory'] ? 'dir' : 'file',
161 | 'dirname' => Util::splitPathIntoDirectoryAndFile($bunny_file_array['Path'])['dir'],
162 | 'guid' => $bunny_file_array['Guid'],
163 | 'object_name' => $bunny_file_array['ObjectName'],
164 | 'timestamp' => self::parse_bunny_timestamp($bunny_file_array['LastChanged']),
165 | 'server_id' => $bunny_file_array['ServerId'],
166 | 'user_id' => $bunny_file_array['UserId'],
167 | 'date_created' => $bunny_file_array['DateCreated'],
168 | 'storage_zone_name' => $bunny_file_array['StorageZoneName'],
169 | 'storage_zone_id' => $bunny_file_array['StorageZoneId'],
170 | 'checksum' => $bunny_file_array['Checksum'],
171 | 'replicated_zones' => $bunny_file_array['ReplicatedZones'],
172 | ];
173 | }
174 |
175 | /**
176 | * Detects the mime type from the provided file path
177 | *
178 | * @param string $path
179 | * @return string
180 | */
181 | public function detectMimeType(string $path): string
182 | {
183 | try {
184 | $detector = new FinfoMimeTypeDetector();
185 | $mimeType = $detector->detectMimeTypeFromPath($path);
186 |
187 | if (! $mimeType) {
188 | return $detector->detectMimeTypeFromBuffer(stream_get_contents($this->readStream($path), 80));
189 | }
190 |
191 | return $mimeType;
192 | } catch (Exception) {
193 | return '';
194 | }
195 | }
196 |
197 | /**
198 | * @param $path
199 | * @param $contents
200 | * @param Config $config
201 | * @return void
202 | */
203 | public function writeStream($path, $contents, Config $config): void
204 | {
205 | $this->write($path, stream_get_contents($contents), $config);
206 | }
207 |
208 | /**
209 | * @param WriteBatchFile[] $writeBatches
210 | * @param Config $config
211 | * @return void
212 | */
213 | public function writeBatch(array $writeBatches, Config $config): void
214 | {
215 | $concurrency = (int) $config->get('concurrency', 50);
216 |
217 | foreach (\array_chunk($writeBatches, $concurrency) as $batch) {
218 | $requests = function () use ($batch) {
219 | /** @var WriteBatchFile $file */
220 | foreach ($batch as $file) {
221 | yield $this->client->getUploadRequest($file->targetPath, \file_get_contents($file->localPath));
222 | }
223 | };
224 |
225 | $pool = new Pool($this->client->guzzleClient, $requests(), [
226 | 'concurrency' => $concurrency,
227 | 'rejected' => function (RequestException|RuntimeException $reason, int $index) {
228 | throw UnableToWriteFile::atLocation($index, $reason->getMessage());
229 | },
230 | ]);
231 |
232 | $pool->promise()->wait();
233 | }
234 | }
235 |
236 | /**
237 | * @param $path
238 | * @return resource
239 | *
240 | * @throws UnableToReadFile
241 | */
242 | public function readStream($path)
243 | {
244 | try {
245 | return $this->client->stream($path);
246 | // @codeCoverageIgnoreStart
247 | } catch (Exceptions\BunnyCDNException|Exceptions\NotFoundException $e) {
248 | throw UnableToReadFile::fromLocation($path, $e->getMessage());
249 | }
250 | // @codeCoverageIgnoreEnd
251 | }
252 |
253 | /**
254 | * @throws UnableToDeleteDirectory
255 | * @throws FilesystemException
256 | */
257 | public function deleteDirectory(string $path): void
258 | {
259 | try {
260 | $this->client->delete(
261 | rtrim($path, '/').'/'
262 | );
263 | // @codeCoverageIgnoreStart
264 | } catch (NotFoundException) {
265 | // nth
266 | } catch (Exceptions\BunnyCDNException $e) {
267 | throw UnableToDeleteDirectory::atLocation($path, $e->getMessage());
268 | }
269 | // @codeCoverageIgnoreEnd
270 | }
271 |
272 | /**
273 | * @throws UnableToCreateDirectory
274 | * @throws FilesystemException
275 | */
276 | public function createDirectory(string $path, Config $config): void
277 | {
278 | try {
279 | $this->client->make_directory($path);
280 | // @codeCoverageIgnoreStart
281 | } catch (Exceptions\BunnyCDNException $e) {
282 | // Lol apparently this is "idempotent" but there's an exception... Sure whatever..
283 | match ($e->getMessage()) {
284 | 'Directory already exists' => '',
285 | default => throw UnableToCreateDirectory::atLocation($path, $e->getMessage())
286 | };
287 | }
288 | // @codeCoverageIgnoreEnd
289 | }
290 |
291 | /**
292 | * @throws InvalidVisibilityProvided
293 | * @throws FilesystemException
294 | */
295 | public function setVisibility(string $path, string $visibility): void
296 | {
297 | throw UnableToSetVisibility::atLocation($path, 'BunnyCDN does not support visibility');
298 | }
299 |
300 | /**
301 | * @throws UnableToRetrieveMetadata
302 | */
303 | public function visibility(string $path): FileAttributes
304 | {
305 | try {
306 | return new FileAttributes($this->getObject($path)->path(), null, $this->pullzone_url ? 'public' : 'private');
307 | } catch (UnableToReadFile|TypeError $e) {
308 | throw new UnableToRetrieveMetadata($e->getMessage());
309 | }
310 | }
311 |
312 | /**
313 | * @param string $path
314 | * @return FileAttributes
315 | *
316 | * @codeCoverageIgnore
317 | */
318 | public function mimeType(string $path): FileAttributes
319 | {
320 | try {
321 | $object = $this->getObject($path);
322 |
323 | if ($object instanceof DirectoryAttributes) {
324 | throw new TypeError();
325 | }
326 |
327 | /** @var FileAttributes $object */
328 | if (! $object->mimeType()) {
329 | $mimeType = $this->detectMimeType($path);
330 |
331 | if (! $mimeType || $mimeType === 'text/plain') { // Really not happy about this being required by Fly's Test case
332 | throw new UnableToRetrieveMetadata('Unknown Mimetype');
333 | }
334 |
335 | return new FileAttributes(
336 | $path,
337 | null,
338 | null,
339 | null,
340 | $mimeType
341 | );
342 | }
343 |
344 | return $object;
345 | } catch (UnableToReadFile $e) {
346 | throw new UnableToRetrieveMetadata($e->getMessage());
347 | } catch (TypeError) {
348 | throw new UnableToRetrieveMetadata('Cannot retrieve mimeType of folder');
349 | }
350 | }
351 |
352 | /**
353 | * @param string $path
354 | * @return mixed
355 | */
356 | protected function getObject(string $path = ''): StorageAttributes
357 | {
358 | $directory = pathinfo($path, PATHINFO_DIRNAME);
359 | $list = (new DirectoryListing($this->listContents($directory, false)))
360 | ->filter(function (StorageAttributes $item) use ($path) {
361 | return Util::normalizePath($item->path()) === $path;
362 | })->toArray();
363 |
364 | if (count($list) === 1) {
365 | return $list[0];
366 | }
367 |
368 | if (count($list) > 1) {
369 | // @codeCoverageIgnoreStart
370 | throw UnableToReadFile::fromLocation($path, 'More than one file was returned for path:"'.$path.'", contact package author.');
371 | // @codeCoverageIgnoreEnd
372 | }
373 |
374 | throw UnableToReadFile::fromLocation($path, 'Error 404:"'.$path.'"');
375 | }
376 |
377 | /**
378 | * @param string $path
379 | * @return FileAttributes
380 | */
381 | public function lastModified(string $path): FileAttributes
382 | {
383 | try {
384 | return $this->getObject($path);
385 | } catch (UnableToReadFile $e) {
386 | throw new UnableToRetrieveMetadata($e->getMessage());
387 | } catch (TypeError) {
388 | throw new UnableToRetrieveMetadata('Last Modified only accepts files as parameters, not directories');
389 | }
390 | }
391 |
392 | /**
393 | * @param string $path
394 | * @return FileAttributes
395 | */
396 | public function fileSize(string $path): FileAttributes
397 | {
398 | try {
399 | return $this->getObject($path);
400 | } catch (UnableToReadFile $e) {
401 | throw new UnableToRetrieveMetadata($e->getMessage());
402 | } catch (TypeError) {
403 | throw new UnableToRetrieveMetadata('Cannot retrieve size of folder');
404 | }
405 | }
406 |
407 | /**
408 | * @throws UnableToMoveFile
409 | * @throws FilesystemException
410 | */
411 | public function move(string $source, string $destination, Config $config): void
412 | {
413 | if ($source === $destination) {
414 | return;
415 | }
416 |
417 | try {
418 | /** @var array $files */
419 | $files = iterator_to_array($this->getFiles($source));
420 |
421 | $sourceLength = \strlen($source);
422 |
423 | foreach ($files as $file) {
424 | $this->moveFile($file, $destination.\substr($file, $sourceLength), $config);
425 | }
426 | } catch (UnableToReadFile $e) {
427 | throw new UnableToMoveFile($e->getMessage());
428 | }
429 | }
430 |
431 | private function getFiles(string $source): iterable
432 | {
433 | $contents = iterator_to_array($this->listContents($source, true));
434 |
435 | if (\count($contents) === 0) {
436 | yield $source;
437 |
438 | return;
439 | }
440 |
441 | /** @var StorageAttributes $entry */
442 | foreach ($contents as $entry) {
443 | if ($entry->isFile() === false) {
444 | continue;
445 | }
446 |
447 | yield $entry->path();
448 | }
449 | }
450 |
451 | private function moveFile(string $source, string $destination, Config $config): void
452 | {
453 | $this->copyFile($source, $destination, $config);
454 | $this->delete($source);
455 | }
456 |
457 | private function copyFile(string $source, string $destination, Config $config): void
458 | {
459 | $this->write($destination, $this->read($source), $config);
460 | }
461 |
462 | /**
463 | * @param $path
464 | * @return void
465 | */
466 | public function delete($path): void
467 | {
468 | // if path is empty or ends with /, it's a directory.
469 | if (empty($path) || str_ends_with($path, '/')) {
470 | throw UnableToDeleteFile::atLocation($path, 'Deletion of directories prevented.');
471 | }
472 |
473 | try {
474 | $this->client->delete($path);
475 | // @codeCoverageIgnoreStart
476 | } catch (NotFoundException) {
477 | // nth
478 | } catch (Exceptions\BunnyCDNException $e) {
479 | throw UnableToDeleteFile::atLocation($path, $e->getMessage());
480 | }
481 | // @codeCoverageIgnoreEnd
482 | }
483 |
484 | /**
485 | * @throws UnableToCheckExistence
486 | */
487 | public function directoryExists(string $path): bool
488 | {
489 | return $this->exists(StorageAttributes::TYPE_DIRECTORY, $path);
490 | }
491 |
492 | /**
493 | * @param string $path
494 | * @return bool
495 | */
496 | public function fileExists(string $path): bool
497 | {
498 | return $this->exists(StorageAttributes::TYPE_FILE, $path);
499 | }
500 |
501 | /**
502 | * @param string $path
503 | * @param Config $config
504 | * @return string
505 | */
506 | public function checksum(string $path, Config $config): string
507 | {
508 | // for compatibility reasons, the default checksum algorithm is md5
509 | $algo = $config->get('checksum_algo', 'md5');
510 |
511 | if ($algo !== 'sha256') {
512 | return $this->calculateChecksumFromStream($path, $config);
513 | }
514 |
515 | try {
516 | $file = $this->getObject($path);
517 | } catch (UnableToReadFile $exception) {
518 | throw new UnableToProvideChecksum($exception->reason(), $path, $exception);
519 | }
520 |
521 | $metaData = $file->extraMetadata();
522 |
523 | if (empty($metaData['checksum']) || ! is_string($metaData['checksum'])) {
524 | throw new UnableToProvideChecksum('Checksum not available.', $path);
525 | }
526 |
527 | return \strtolower($metaData['checksum']);
528 | }
529 |
530 | /**
531 | * @deprecated use publicUrl instead
532 | *
533 | * @param string $path
534 | * @return string
535 | * @codeCoverageIgnore
536 | * @noinspection PhpUnused
537 | */
538 | public function getUrl(string $path): string
539 | {
540 | return $this->publicUrl($path, new Config());
541 | }
542 |
543 | /**
544 | * @param string $path
545 | * @param Config $config
546 | * @return string
547 | */
548 | public function publicUrl(string $path, Config $config): string
549 | {
550 | if ($this->pullzone_url === '') {
551 | throw new RuntimeException('In order to get a visible URL for a BunnyCDN object, you must pass the "pullzone_url" parameter to the BunnyCDNAdapter.');
552 | }
553 |
554 | return rtrim($this->pullzone_url, '/').'/'.ltrim($path, '/');
555 | }
556 |
557 | private static function parse_bunny_timestamp(string $timestamp): int
558 | {
559 | return (date_create_from_format('Y-m-d\TH:i:s.u', $timestamp) ?: date_create_from_format('Y-m-d\TH:i:s', $timestamp))->getTimestamp();
560 | }
561 |
562 | private function exists(string $type, string $path): bool
563 | {
564 | $list = new DirectoryListing($this->listContents(
565 | Util::splitPathIntoDirectoryAndFile($path)['dir'],
566 | false
567 | ));
568 |
569 | $count = $list->filter(function (StorageAttributes $item) use ($path, $type) {
570 | return $item->type() === $type && Util::normalizePath($item->path()) === Util::normalizePath($path);
571 | })->toArray();
572 |
573 | return (bool) count($count);
574 | }
575 | }
576 |
--------------------------------------------------------------------------------
/src/BunnyCDNClient.php:
--------------------------------------------------------------------------------
1 | guzzleClient = new Guzzle();
22 | }
23 |
24 | private static function get_base_url($region): string
25 | {
26 | return match (strtolower($region)) {
27 | BunnyCDNRegion::NEW_YORK => 'https://ny.storage.bunnycdn.com/',
28 | BunnyCDNRegion::LOS_ANGELAS => 'https://la.storage.bunnycdn.com/',
29 | BunnyCDNRegion::SINGAPORE => 'https://sg.storage.bunnycdn.com/',
30 | BunnyCDNRegion::SYDNEY => 'https://syd.storage.bunnycdn.com/',
31 | BunnyCDNRegion::UNITED_KINGDOM => 'https://uk.storage.bunnycdn.com/',
32 | BunnyCDNRegion::STOCKHOLM => 'https://se.storage.bunnycdn.com/',
33 | BunnyCDNRegion::BRAZIL => 'https://br.storage.bunnycdn.com/',
34 | BunnyCDNRegion::JOHANNESBURG => 'https://jh.storage.bunnycdn.com/',
35 | default => 'https://storage.bunnycdn.com/'
36 | };
37 | }
38 |
39 | public function createRequest(string $path, string $method = 'GET', array $headers = [], $body = null): Request
40 | {
41 | return new Request(
42 | $method,
43 | self::get_base_url($this->region).Util::normalizePath('/'.$this->storage_zone_name.'/').$path,
44 | array_merge([
45 | 'Accept' => '*/*',
46 | 'AccessKey' => $this->api_key,
47 | ], $headers),
48 | $body
49 | );
50 | }
51 |
52 | /**
53 | * @throws ClientExceptionInterface
54 | */
55 | private function request(Request $request, array $options = []): mixed
56 | {
57 | $contents = $this->guzzleClient->send($request, $options)->getBody()->getContents();
58 |
59 | return json_decode($contents, true) ?? $contents;
60 | }
61 |
62 | /**
63 | * @param string $path
64 | * @return array
65 | *
66 | * @throws NotFoundException|BunnyCDNException
67 | */
68 | public function list(string $path): array
69 | {
70 | try {
71 | $listing = $this->request($this->createRequest(Util::normalizePath($path).'/'));
72 |
73 | // Throw an exception if we don't get back an array
74 | if (! is_array($listing)) {
75 | throw new NotFoundException('File is not a directory');
76 | }
77 |
78 | return array_map(function ($bunny_cdn_item) {
79 | return $bunny_cdn_item;
80 | }, $listing);
81 | // @codeCoverageIgnoreStart
82 | } catch (GuzzleException $e) {
83 | throw match ($e->getCode()) {
84 | 404 => new NotFoundException($e->getMessage()),
85 | default => new BunnyCDNException($e->getMessage())
86 | };
87 | }
88 | // @codeCoverageIgnoreEnd
89 | }
90 |
91 | /**
92 | * @param string $path
93 | * @return mixed
94 | *
95 | * @throws BunnyCDNException
96 | * @throws NotFoundException
97 | */
98 | public function download(string $path): string
99 | {
100 | try {
101 | $content = $this->request($this->createRequest($path.'?download'));
102 |
103 | if (\is_array($content)) {
104 | return \json_encode($content);
105 | }
106 |
107 | return $content;
108 | // @codeCoverageIgnoreStart
109 | } catch (GuzzleException $e) {
110 | throw match ($e->getCode()) {
111 | 404 => new NotFoundException($e->getMessage()),
112 | default => new BunnyCDNException($e->getMessage())
113 | };
114 | }
115 | // @codeCoverageIgnoreEnd
116 | }
117 |
118 | /**
119 | * @param string $path
120 | * @return resource|null
121 | *
122 | * @throws BunnyCDNException
123 | * @throws NotFoundException
124 | */
125 | public function stream(string $path)
126 | {
127 | try {
128 | return $this->guzzleClient->send($this->createRequest($path), ['stream' => true])->getBody()->detach();
129 | // @codeCoverageIgnoreStart
130 | } catch (GuzzleException $e) {
131 | throw match ($e->getCode()) {
132 | 404 => new NotFoundException($e->getMessage()),
133 | default => new BunnyCDNException($e->getMessage())
134 | };
135 | }
136 | // @codeCoverageIgnoreEnd
137 | }
138 |
139 | public function getUploadRequest(string $path, $contents): Request
140 | {
141 | return $this->createRequest(
142 | $path,
143 | 'PUT',
144 | [
145 | 'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8',
146 | ],
147 | $contents
148 | );
149 | }
150 |
151 | /**
152 | * @param string $path
153 | * @param $contents
154 | * @return mixed
155 | *
156 | * @throws BunnyCDNException
157 | */
158 | public function upload(string $path, $contents): mixed
159 | {
160 | try {
161 | return $this->request($this->getUploadRequest($path, $contents));
162 | // @codeCoverageIgnoreStart
163 | } catch (GuzzleException $e) {
164 | throw new BunnyCDNException($e->getMessage());
165 | }
166 | // @codeCoverageIgnoreEnd
167 | }
168 |
169 | /**
170 | * @param string $path
171 | * @return mixed
172 | *
173 | * @throws BunnyCDNException
174 | */
175 | public function make_directory(string $path): mixed
176 | {
177 | try {
178 | return $this->request($this->createRequest(Util::normalizePath($path).'/', 'PUT', [
179 | 'Content-Length' => 0,
180 | ]));
181 | // @codeCoverageIgnoreStart
182 | } catch (GuzzleException $e) {
183 | throw match ($e->getCode()) {
184 | 400 => new BunnyCDNException('Directory already exists'),
185 | default => new BunnyCDNException($e->getMessage())
186 | };
187 | }
188 | // @codeCoverageIgnoreEnd
189 | }
190 |
191 | /**
192 | * @param string $path
193 | * @return mixed
194 | *
195 | * @throws NotFoundException
196 | * @throws BunnyCDNException
197 | */
198 | public function delete(string $path): mixed
199 | {
200 | try {
201 | return $this->request($this->createRequest($path, 'DELETE'));
202 | // @codeCoverageIgnoreStart
203 | } catch (GuzzleException $e) {
204 | throw match ($e->getCode()) {
205 | 404 => new NotFoundException($e->getMessage()),
206 | default => new BunnyCDNException($e->getMessage())
207 | };
208 | }
209 | // @codeCoverageIgnoreEnd
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/src/BunnyCDNRegion.php:
--------------------------------------------------------------------------------
1 | $file,
25 | 'dir' => $directory,
26 | ];
27 | }
28 |
29 | /**
30 | * @param $path
31 | * @param bool $isDirectory
32 | * @return false|string|string[]
33 | */
34 | public static function normalizePath($path, $isDirectory = false)
35 | {
36 | $path = str_replace('\\', '/', $path);
37 |
38 | if ($isDirectory && ! self::endsWith($path, '/')) {
39 | $path .= '/';
40 | }
41 |
42 | // Remove double slashes
43 | while (strpos($path, '//') !== false) {
44 | $path = str_replace('//', '/', $path);
45 | }
46 |
47 | // Remove the starting slash
48 | if (strpos($path, '/') === 0) {
49 | $path = substr($path, 1);
50 | }
51 |
52 | return $path;
53 | }
54 |
55 | /**
56 | * @codeCoverageIgnore
57 | *
58 | * @param $haystack
59 | * @param $needle
60 | * @return bool
61 | */
62 | public static function startsWith($haystack, $needle): bool
63 | {
64 | return strpos($haystack, $needle) === 0;
65 | }
66 |
67 | /**
68 | * @param $haystack
69 | * @param $needle
70 | * @return bool
71 | */
72 | public static function endsWith($haystack, $needle): bool
73 | {
74 | $length = strlen($needle);
75 | if ($length === 0) {
76 | return true;
77 | }
78 |
79 | return substr($haystack, -$length) === $needle;
80 | }
81 |
82 | public static function replaceFirst(string $search, string $replace, string $subject): string
83 | {
84 | $position = strpos($subject, $search);
85 |
86 | if ($position !== false) {
87 | return (string) substr_replace($subject, $replace, $position, strlen($search));
88 | }
89 |
90 | return $subject;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/WriteBatchFile.php:
--------------------------------------------------------------------------------
1 | client = self::bunnyCDNClient();
36 | $this->clearStorage();
37 | }
38 |
39 | private function clearStorage()
40 | {
41 | foreach ($this->client->list('/') as $item) {
42 | try {
43 | $this->client->delete($item['IsDirectory'] ? $item['ObjectName'].'/' : $item['ObjectName']);
44 | } catch (\Exception $exception) {
45 | } // Try our best effort at removing everything from the filesystem
46 | }
47 |
48 | assertEmpty(
49 | $this->client->list('/'),
50 | 'Warning! Bunny Client not emptied out prior to next test. This can be problematic when running the test against production clients'
51 | );
52 | }
53 |
54 | protected function tearDown(): void
55 | {
56 | $this->clearStorage();
57 | }
58 |
59 | /**
60 | * @return void
61 | *
62 | * @throws NotFoundException
63 | * @throws BunnyCDNException
64 | */
65 | public function test_listing_directory()
66 | {
67 | // Arrange
68 | $this->client->make_directory('subfolder');
69 | $this->client->upload('example_image.png', 'test');
70 |
71 | $response = $this->client->list('/');
72 |
73 | $this->assertIsArray($response);
74 | $this->assertCount(2, $response);
75 | }
76 |
77 | /**
78 | * @return void
79 | *
80 | * @throws NotFoundException
81 | * @throws BunnyCDNException
82 | */
83 | public function test_listing_subdirectory()
84 | {
85 | // Arrange
86 | $this->client->upload('/subfolder/example_image.png', 'test');
87 |
88 | // Act
89 | $response = $this->client->list('/subfolder');
90 |
91 | // Assert
92 | $this->assertIsArray($response);
93 | $this->assertCount(1, $response);
94 | }
95 |
96 | /**
97 | * @return void
98 | *
99 | * @throws BunnyCDNException
100 | * @throws NotFoundException
101 | */
102 | public function test_download_file()
103 | {
104 | $this->client->upload('/test.png', 'test');
105 |
106 | $response = $this->client->download('/test.png');
107 |
108 | $this->assertIsString($response);
109 | }
110 |
111 | /**
112 | * @return void
113 | *
114 | * @throws BunnyCDNException
115 | * @throws NotFoundException
116 | */
117 | public function test_streaming()
118 | {
119 | $this->client->upload('/test.png', str_repeat('example_image_contents', 1024));
120 |
121 | $stream = $this->client->stream('/test.png');
122 |
123 | $this->assertIsResource($stream);
124 |
125 | do {
126 | $line = stream_get_line($stream, 512);
127 | $this->assertStringContainsString('example_image_contents', $line);
128 | $this->assertEquals(512, strlen($line));
129 | } while ($line && strlen($line) > 512);
130 | }
131 |
132 | /**
133 | * @return void
134 | *
135 | * @throws BunnyCDNException
136 | */
137 | public function test_upload()
138 | {
139 | $response = $this->client->upload('/test_contents.txt', 'testing_contents');
140 |
141 | $this->assertIsArray($response);
142 |
143 | $this->assertEquals([
144 | 'HttpCode' => 201,
145 | 'Message' => 'File uploaded.',
146 | ], $response);
147 | }
148 |
149 | /**
150 | * @return void
151 | *
152 | * @throws BunnyCDNException
153 | */
154 | public function test_make_directory()
155 | {
156 | $response = $this->client->make_directory('/test_dir/');
157 |
158 | $this->assertIsArray($response);
159 | $this->assertEquals([
160 | 'HttpCode' => 201,
161 | 'Message' => 'Directory created.',
162 | ], $response);
163 | }
164 |
165 | /**
166 | * @return void
167 | *
168 | * @throws BunnyCDNException
169 | * @throws NotFoundException
170 | */
171 | public function test_delete_file()
172 | {
173 | $this->client->upload('test_file.txt', '123');
174 |
175 | $response = $this->client->delete('/test_file.txt');
176 |
177 | $this->assertIsArray($response);
178 | $this->assertEquals([
179 | 'HttpCode' => 200,
180 | 'Message' => 'File deleted successfuly.', // ಠ_ಠ Spelling @bunny.net
181 | ], $response);
182 | }
183 |
184 | /**
185 | * @return void
186 | *
187 | * @throws BunnyCDNException
188 | * @throws NotFoundException
189 | */
190 | public function test_delete_file_not_found()
191 | {
192 | $this->expectException(NotFoundException::class);
193 | $this->client->delete('file_not_found.txt');
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/tests/FlysystemAdapterTest.php:
--------------------------------------------------------------------------------
1 | guzzleClient = new Guzzle([
65 | 'handler' => function (Request $request) use ($mockedClient) {
66 | $path = $request->getUri()->getPath();
67 | $method = $request->getMethod();
68 |
69 | if ($method === 'PUT' && $path === 'destination.txt') {
70 | $mockedClient->filesystem->write('destination.txt', 'text');
71 |
72 | return new Response(200);
73 | }
74 |
75 | if ($method === 'PUT' && $path === 'destination2.txt') {
76 | $mockedClient->filesystem->write('destination2.txt', 'text2');
77 |
78 | return new Response(200);
79 | }
80 |
81 | if ($method === 'PUT' && \in_array($path, ['failing.txt', 'failing2.txt'])) {
82 | throw new \RuntimeException('Failed to write file');
83 | }
84 |
85 | throw new \RuntimeException('Unexpected request: '.$method.' '.$path);
86 | },
87 | ]);
88 |
89 | return $mockedClient;
90 | }
91 |
92 | public static function createFilesystemAdapter(): FilesystemAdapter
93 | {
94 | return new BunnyCDNAdapter(self::bunnyCDNClient(), static::$publicUrl);
95 | }
96 |
97 | /**
98 | * Skipped
99 | */
100 | public function setting_visibility(): void
101 | {
102 | $this->markTestSkipped('No visibility support is provided for BunnyCDN');
103 | }
104 |
105 | public function generating_a_temporary_url(): void
106 | {
107 | $this->markTestSkipped('No temporary URL support is provided for BunnyCDN');
108 | }
109 |
110 | /**
111 | * @test
112 | */
113 | public function file_exists_on_directory_is_false(): void
114 | {
115 | $this->runScenario(function () {
116 | $adapter = $this->adapter();
117 |
118 | $this->assertFalse($adapter->directoryExists('test'));
119 | $adapter->createDirectory('test', new Config());
120 | $this->assertTrue($adapter->directoryExists('test'));
121 | $this->assertFalse($adapter->fileExists('test'));
122 | });
123 | }
124 |
125 | /**
126 | * @test
127 | */
128 | public function directory_exists_on_file_is_false(): void
129 | {
130 | $this->runScenario(function () {
131 | $adapter = $this->adapter();
132 |
133 | $this->assertFalse($adapter->fileExists('test.txt'));
134 | $adapter->write('test.txt', 'aaa', new Config());
135 | $this->assertTrue($adapter->fileExists('test.txt'));
136 | $this->assertFalse($adapter->directoryExists('test.txt'));
137 | });
138 | }
139 |
140 | /**
141 | * @test
142 | */
143 | public function delete_on_directory_throws_exception(): void
144 | {
145 | $this->runScenario(function () {
146 | $adapter = $this->adapter();
147 |
148 | $adapter->write(
149 | 'test/text.txt',
150 | 'contents',
151 | new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])
152 | );
153 |
154 | $this->expectException(UnableToDeleteFile::class);
155 | $adapter->delete('test/');
156 | });
157 | }
158 |
159 | /**
160 | * @test
161 | */
162 | public function delete_with_empty_path_throws_exception(): void
163 | {
164 | $this->runScenario(function () {
165 | $adapter = $this->adapter();
166 |
167 | $adapter->write(
168 | 'test/text.txt',
169 | 'contents',
170 | new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])
171 | );
172 |
173 | $this->expectException(UnableToDeleteFile::class);
174 | $adapter->delete('');
175 | });
176 | }
177 |
178 | /**
179 | * @test
180 | */
181 | public function moving_a_folder(): void
182 | {
183 | $this->runScenario(function () {
184 | $adapter = $this->adapter();
185 | $adapter->write(
186 | 'test/text.txt',
187 | 'contents to be copied',
188 | new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])
189 | );
190 | $adapter->write(
191 | 'test/2/text.txt',
192 | 'contents to be copied',
193 | new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])
194 | );
195 | $adapter->move('test', 'destination', new Config());
196 | $this->assertFalse(
197 | $adapter->fileExists('test/text.txt'),
198 | 'After moving a file should no longer exist in the original location.'
199 | );
200 | $this->assertFalse(
201 | $adapter->fileExists('test/2/text.txt'),
202 | 'After moving a file should no longer exist in the original location.'
203 | );
204 | $this->assertTrue(
205 | $adapter->fileExists('destination/text.txt'),
206 | 'After moving, a file should be present at the new location.'
207 | );
208 | $this->assertTrue(
209 | $adapter->fileExists('destination/2/text.txt'),
210 | 'After moving, a file should be present at the new location.'
211 | );
212 | $this->assertEquals('contents to be copied', $adapter->read('destination/text.txt'));
213 | $this->assertEquals('contents to be copied', $adapter->read('destination/2/text.txt'));
214 | });
215 | }
216 |
217 | /**
218 | * @test
219 | */
220 | public function moving_a_not_existing_folder(): void
221 | {
222 | $this->runScenario(function () {
223 | $adapter = $this->adapter();
224 |
225 | $this->expectException(UnableToMoveFile::class);
226 | $adapter->move('not_existing_file', 'destination', new Config());
227 | });
228 | }
229 |
230 | /**
231 | * @test
232 | */
233 | public function copying_a_folder(): void
234 | {
235 | $this->runScenario(function () {
236 | $adapter = $this->adapter();
237 | $adapter->write(
238 | 'test/text.txt',
239 | 'contents to be copied',
240 | new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])
241 | );
242 | $adapter->write(
243 | 'test/2/text.txt',
244 | 'contents to be copied',
245 | new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])
246 | );
247 | $adapter->copy('test', 'destination', new Config());
248 | $this->assertTrue(
249 | $adapter->fileExists('test/text.txt'),
250 | 'After copying a file should exist in the original location.'
251 | );
252 | $this->assertTrue(
253 | $adapter->fileExists('test/2/text.txt'),
254 | 'After copying a file should exist in the original location.'
255 | );
256 | $this->assertTrue(
257 | $adapter->fileExists('destination/text.txt'),
258 | 'After copying, a file should be present at the new location.'
259 | );
260 | $this->assertTrue(
261 | $adapter->fileExists('destination/2/text.txt'),
262 | 'After copying, a file should be present at the new location.'
263 | );
264 | $this->assertEquals('contents to be copied', $adapter->read('destination/text.txt'));
265 | $this->assertEquals('contents to be copied', $adapter->read('destination/2/text.txt'));
266 | });
267 | }
268 |
269 | /**
270 | * @test
271 | */
272 | public function copying_a_not_existing_folder(): void
273 | {
274 | $this->runScenario(function () {
275 | $adapter = $this->adapter();
276 |
277 | $this->expectException(UnableToCopyFile::class);
278 | $adapter->copy('not_existing_file', 'destination', new Config());
279 | });
280 | }
281 |
282 | /**
283 | * We overwrite the test, because the original tries accessing the url
284 | *
285 | * @test
286 | */
287 | public function generating_a_public_url(): void
288 | {
289 | if (self::$isLive && ! \str_starts_with(static::$publicUrl, self::DEMOURL)) {
290 | parent::generating_a_public_url();
291 |
292 | return;
293 | }
294 |
295 | $url = $this->adapter()->publicUrl('/path.txt', new Config());
296 |
297 | self::assertEquals(static::$publicUrl.'/path.txt', $url);
298 | }
299 |
300 | public function test_without_pullzone_url_error_thrown_accessing_url(): void
301 | {
302 | $this->expectException(\RuntimeException::class);
303 | $this->expectExceptionMessage('In order to get a visible URL for a BunnyCDN object, you must pass the "pullzone_url" parameter to the BunnyCDNAdapter.');
304 | $myAdapter = new BunnyCDNAdapter(static::bunnyCDNClient());
305 | $myAdapter->publicUrl('/path.txt', new Config());
306 | }
307 |
308 | /**
309 | * @test
310 | */
311 | public function overwriting_a_file(): void
312 | {
313 | $this->runScenario(function () {
314 | $this->givenWeHaveAnExistingFile('path.txt', 'contents', ['visibility' => Visibility::PUBLIC]);
315 | $adapter = $this->adapter();
316 |
317 | $adapter->write('path.txt', 'new contents', new Config(['visibility' => Visibility::PRIVATE]));
318 |
319 | $contents = $adapter->read('path.txt');
320 | $this->assertEquals('new contents', $contents);
321 | // $visibility = $adapter->visibility('path.txt')->visibility();
322 | // $this->assertEquals(Visibility::PRIVATE, $visibility); // Commented out of this test
323 | });
324 | }
325 |
326 | /**
327 | * @test
328 | */
329 | public function moving_a_file_to_same_destination(): void
330 | {
331 | $this->runScenario(function () {
332 | $adapter = $this->adapter();
333 | $adapter->write(
334 | 'source.txt',
335 | 'contents to be copied',
336 | new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])
337 | );
338 | $adapter->move('source.txt', 'source.txt', new Config());
339 | $this->assertTrue(
340 | $adapter->fileExists('source.txt'),
341 | 'After moving a file to the same location the file should exist.'
342 | );
343 | });
344 | }
345 |
346 | /**
347 | * @test
348 | */
349 | public function get_checksum(): void
350 | {
351 | $adapter = $this->adapter();
352 |
353 | $adapter->write('path.txt', 'foobar', new Config());
354 |
355 | $this->assertSame(
356 | '3858f62230ac3c915f300c664312c63f',
357 | $adapter->checksum('path.txt', new Config())
358 | );
359 |
360 | $this->assertSame(
361 | 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2',
362 | $adapter->checksum('path.txt', new Config(['checksum_algo' => 'sha256']))
363 | );
364 | }
365 |
366 | public function test_checksum_throws_error_with_non_existing_file_on_default_algo(): void
367 | {
368 | $adapter = $this->adapter();
369 |
370 | $this->expectException(UnableToProvideChecksum::class);
371 | $adapter->checksum('path.txt', new Config(['checksum_algo' => 'sha256']));
372 | }
373 |
374 | //test_checksum_throws_error_with_empty_checksum_from_client
375 | public function test_checksum_throws_error_with_empty_checksum_from_client(): void
376 | {
377 | $client = $this->createMock(BunnyCDNClient::class);
378 | $client->expects(self::exactly(1))->method('list')->willReturnCallback(
379 | function () {
380 | ['file' => $file, 'dir' => $dir] = Util::splitPathIntoDirectoryAndFile('file.txt');
381 | $dir = Util::normalizePath($dir);
382 | $faker = Factory::create();
383 | $storage_zone = $faker->word;
384 |
385 | return [[
386 | 'Guid' => $faker->uuid,
387 | 'StorageZoneName' => $storage_zone,
388 | 'Path' => Util::normalizePath('/'.$storage_zone.'/'.$dir.'/'),
389 | 'ObjectName' => $file,
390 | 'Length' => $faker->numberBetween(0, 10240),
391 | 'LastChanged' => date('Y-m-d\TH:i:s.v'),
392 | 'ServerId' => $faker->numberBetween(0, 10240),
393 | 'ArrayNumber' => 0,
394 | 'IsDirectory' => false,
395 | 'UserId' => 'bf91bc4e-0e60-411a-b475-4416926d20f7',
396 | 'ContentType' => '',
397 | 'DateCreated' => date('Y-m-d\TH:i:s.v'),
398 | 'StorageZoneId' => $faker->numberBetween(0, 102400),
399 | 'Checksum' => null,
400 | 'ReplicatedZones' => '',
401 | ]];
402 | }
403 | );
404 |
405 | $adapter = new BunnyCDNAdapter($client);
406 | $this->expectException(UnableToProvideChecksum::class);
407 | $this->expectExceptionMessage('Unable to get checksum for file.txt: Checksum not available.');
408 | $adapter->checksum('file.txt', new Config(['checksum_algo' => 'sha256']));
409 | }
410 |
411 | /**
412 | * @test
413 | */
414 | public function fetching_the_mime_type_of_an_svg_file_by_file_name(): void
415 | {
416 | $this->runScenario(function () {
417 | $adapter = $this->adapter();
418 | $adapter->write(
419 | 'source.svg',
420 | '',
421 | new Config()
422 | );
423 |
424 | $this->assertSame(
425 | 'image/svg+xml',
426 | $adapter->detectMimeType('source.svg')
427 | );
428 | });
429 | }
430 |
431 | /**
432 | * Fix issue where `fopen` complains when opening downloaded image file#20
433 | * https://github.com/PlatformCommunity/flysystem-bunnycdn/pull/20
434 | *
435 | * @return void
436 | *
437 | * @throws FilesystemException
438 | * @throws Throwable
439 | */
440 | public function test_regression_pr_20()
441 | {
442 | $image = base64_decode('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==');
443 | $this->givenWeHaveAnExistingFile('path.png', $image);
444 |
445 | $this->runScenario(function () use ($image) {
446 | $adapter = $this->adapter();
447 |
448 | $stream = $adapter->readStream('path.png');
449 |
450 | $this->assertIsResource($stream);
451 | $this->assertEquals($image, stream_get_contents($stream));
452 | });
453 | }
454 |
455 | /**
456 | * Github Issue - 28
457 | * https://github.com/PlatformCommunity/flysystem-bunnycdn/issues/28
458 | *
459 | * Issue present where a lot of TypeErrors will appear if you ask for lastModified on Directory (returns FileAttributes)
460 | *
461 | * @throws FilesystemException
462 | */
463 | public function test_regression_issue_29()
464 | {
465 | $client = self::bunnyCDNClient();
466 | $client->make_directory('/example_folder');
467 |
468 | $adapter = new Filesystem(new BunnyCDNAdapter($client));
469 | $exception_count = 0;
470 |
471 | try {
472 | $adapter->fileSize('/example_folder');
473 | } catch (\Exception $e) {
474 | $this->assertInstanceOf(UnableToRetrieveMetadata::class, $e);
475 | $exception_count++;
476 | }
477 |
478 | try {
479 | $adapter->mimeType('/example_folder');
480 | } catch (\Exception $e) {
481 | $this->assertInstanceOf(UnableToRetrieveMetadata::class, $e);
482 | $exception_count++;
483 | }
484 |
485 | try {
486 | $adapter->lastModified('/example_folder');
487 | } catch (\Exception $e) {
488 | $this->assertInstanceOf(UnableToRetrieveMetadata::class, $e);
489 | $exception_count++;
490 | }
491 |
492 | // The fact that PHPUnit makes me do this is 🤬
493 | $this->assertEquals(3, $exception_count);
494 | }
495 |
496 | /**
497 | * Github Issue - 39
498 | * https://github.com/PlatformCommunity/flysystem-bunnycdn/issues/39
499 | *
500 | * Can't request file containing json
501 | *
502 | * @throws FilesystemException
503 | */
504 | public function test_regression_issue_39()
505 | {
506 | $this->runScenario(function () {
507 | $adapter = $this->adapter();
508 |
509 | $adapter->write('test.json', json_encode(['test' => 123]), new Config([]));
510 |
511 | $response = $adapter->read('/test.json');
512 |
513 | $this->assertIsString($response);
514 | });
515 | }
516 |
517 | public function test_write_batch(): void
518 | {
519 | $this->runScenario(function () {
520 | $firstTmpFile = \tmpfile();
521 | fwrite($firstTmpFile, 'text');
522 | $firstTmpPath = stream_get_meta_data($firstTmpFile)['uri'];
523 |
524 | $secondTmpFile = \tmpfile();
525 | fwrite($secondTmpFile, 'text2');
526 | $secondTmpPath = stream_get_meta_data($secondTmpFile)['uri'];
527 |
528 | $adapter = $this->adapter();
529 |
530 | $adapter->writeBatch(
531 | [
532 | new WriteBatchFile($firstTmpPath, 'destination.txt'),
533 | new WriteBatchFile($secondTmpPath, 'destination2.txt'),
534 | ],
535 | new Config()
536 | );
537 |
538 | \fclose($firstTmpFile);
539 | \fclose($secondTmpFile);
540 |
541 | $this->assertSame('text', $adapter->read('destination.txt'));
542 | $this->assertSame('text2', $adapter->read('destination2.txt'));
543 | });
544 | }
545 |
546 | public function test_failing_write_batch(): void
547 | {
548 | if (self::$isLive) {
549 | $this->markTestSkipped('This test is not applicable in live mode');
550 | }
551 |
552 | $this->runScenario(function () {
553 | $firstTmpFile = \tmpfile();
554 | fwrite($firstTmpFile, 'text');
555 | $firstTmpPath = stream_get_meta_data($firstTmpFile)['uri'];
556 |
557 | $secondTmpFile = \tmpfile();
558 | fwrite($secondTmpFile, 'text2');
559 | $secondTmpPath = stream_get_meta_data($firstTmpFile)['uri'];
560 |
561 | $adapter = $this->adapter();
562 |
563 | $this->expectException(UnableToWriteFile::class);
564 | $adapter->writeBatch(
565 | [
566 | new WriteBatchFile($firstTmpPath, 'failing.txt'),
567 | new WriteBatchFile($secondTmpPath, 'failing2.txt'),
568 | ],
569 | new Config()
570 | );
571 |
572 | \fclose($firstTmpFile);
573 | \fclose($secondTmpFile);
574 | });
575 | }
576 | }
577 |
--------------------------------------------------------------------------------
/tests/MockClient.php:
--------------------------------------------------------------------------------
1 | filesystem = new Filesystem(new InMemoryFilesystemAdapter());
31 | }
32 |
33 | /**
34 | * @param string $path
35 | * @return array
36 | */
37 | public function list(string $path): array
38 | {
39 | try {
40 | return $this->filesystem->listContents($path)->map(function (StorageAttributes $file) {
41 | return ! $file instanceof FileAttributes
42 | ? self::example_folder($file->path(), $this->storage_zone_name, [])
43 | : self::example_file($file->path(), $this->storage_zone_name, [
44 | 'Length' => $file->fileSize(),
45 | 'Checksum' => hash('sha256', $this->filesystem->read($file->path())),
46 | ]);
47 | })->toArray();
48 | } catch (FilesystemException) {
49 | }
50 |
51 | return [];
52 | }
53 |
54 | /**
55 | * @param string $path
56 | * @return string
57 | *
58 | * @throws FilesystemException
59 | */
60 | public function download(string $path): string
61 | {
62 | return $this->filesystem->read($path);
63 | }
64 |
65 | /**
66 | * @param string $path
67 | * @return resource
68 | *
69 | * @throws FilesystemException
70 | */
71 | public function stream(string $path)
72 | {
73 | return $this->filesystem->readStream($path);
74 | }
75 |
76 | /**
77 | * @param string $path
78 | * @param $contents
79 | * @return array
80 | */
81 | public function upload(string $path, $contents): array
82 | {
83 | try {
84 | $this->filesystem->write($path, $contents);
85 |
86 | return [
87 | 'HttpCode' => 201,
88 | 'Message' => 'File uploaded.',
89 | ];
90 | } catch (FilesystemException) {
91 | }
92 |
93 | return [];
94 | }
95 |
96 | /**
97 | * @param string $path
98 | * @return array
99 | */
100 | public function make_directory(string $path): array
101 | {
102 | try {
103 | $this->filesystem->createDirectory($path);
104 |
105 | return [
106 | 'HttpCode' => 201,
107 | 'Message' => 'Directory created.',
108 | ];
109 | } catch (FilesystemException) {
110 | }
111 |
112 | return [];
113 | }
114 |
115 | /**
116 | * @param string $path
117 | * @return array
118 | *
119 | * @throws FilesystemException
120 | * @throws BunnyCDNException
121 | * @throws NotFoundException
122 | */
123 | public function delete(string $path): array
124 | {
125 | try {
126 | $this->filesystem->has($path) ?
127 | $this->filesystem->deleteDirectory($path) || $this->filesystem->delete($path) :
128 | throw new NotFoundException();
129 |
130 | return [
131 | 'HttpCode' => 200,
132 | 'Message' => 'File deleted successfuly.', // ಠ_ಠ Spelling @bunny.net
133 | ];
134 | } catch (NotFoundException) {
135 | throw new NotFoundException('404');
136 | } catch (\Exception) {
137 | return [
138 | 'HttpCode' => 404,
139 | 'Message' => 'File deleted successfuly.', // ಠ_ಠ Spelling @bunny.net
140 | ];
141 | }
142 | }
143 |
144 | public function getUploadRequest(string $path, $contents): Request
145 | {
146 | return new Request('PUT', $path, [], $contents);
147 | }
148 |
149 | private static function example_file($path = '/directory/test.png', $storage_zone = 'storage_zone', $override = []): array
150 | {
151 | ['file' => $file, 'dir' => $dir] = Util::splitPathIntoDirectoryAndFile($path);
152 | $dir = Util::normalizePath($dir);
153 | $faker = Factory::create();
154 |
155 | return array_merge([
156 | 'Guid' => $faker->uuid,
157 | 'StorageZoneName' => $storage_zone,
158 | 'Path' => Util::normalizePath('/'.$storage_zone.'/'.$dir.'/'),
159 | 'ObjectName' => $file,
160 | 'Length' => $faker->numberBetween(0, 10240),
161 | 'LastChanged' => date('Y-m-d\TH:i:s.v'),
162 | 'ServerId' => $faker->numberBetween(0, 10240),
163 | 'ArrayNumber' => 0,
164 | 'IsDirectory' => false,
165 | 'UserId' => 'bf91bc4e-0e60-411a-b475-4416926d20f7',
166 | 'ContentType' => '',
167 | 'DateCreated' => date('Y-m-d\TH:i:s.v'),
168 | 'StorageZoneId' => $faker->numberBetween(0, 102400),
169 | 'Checksum' => strtoupper($faker->sha256),
170 | 'ReplicatedZones' => '',
171 | ], $override);
172 | }
173 |
174 | private static function example_folder($path = '/directory/', $storage_zone = 'storage_zone', $override = []): array
175 | {
176 | ['file' => $file, 'dir' => $dir] = Util::splitPathIntoDirectoryAndFile($path);
177 | $dir = Util::normalizePath($dir);
178 | $faker = Factory::create();
179 |
180 | return array_merge([
181 | 'Guid' => $faker->uuid,
182 | 'StorageZoneName' => $storage_zone,
183 | 'Path' => Util::normalizePath('/'.$storage_zone.'/'.$dir.'/'),
184 | 'ObjectName' => $file,
185 | 'Length' => 0,
186 | 'LastChanged' => date('Y-m-d\TH:i:s.v'),
187 | 'ServerId' => $faker->numberBetween(0, 10240),
188 | 'ArrayNumber' => 0,
189 | 'IsDirectory' => true,
190 | 'UserId' => 'bf91bc4e-0e60-411a-b475-4416926d20f7',
191 | 'ContentType' => '',
192 | 'DateCreated' => date('Y-m-d\TH:i:s.v'),
193 | 'StorageZoneId' => $faker->numberBetween(0, 102400),
194 | 'Checksum' => '',
195 | 'ReplicatedZones' => '',
196 | ], $override);
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/tests/PrefixTest.php:
--------------------------------------------------------------------------------
1 | deleteDirectory('/'.self::PREFIX_PATH);
56 | } catch (FilesystemException $e) {
57 | }
58 | }
59 |
60 | /**
61 | * Skipped
62 | */
63 | public function setting_visibility(): void
64 | {
65 | $this->markTestSkipped('No visibility support is provided for BunnyCDN');
66 | }
67 |
68 | public function generating_a_temporary_url(): void
69 | {
70 | $this->markTestSkipped('No temporary URL support is provided for BunnyCDN');
71 | }
72 |
73 | /**
74 | * Overwritten (usually because of visibility)
75 | */
76 |
77 | /**
78 | * We overwrite the test, because the original tries accessing the url
79 | *
80 | * @test
81 | */
82 | public function generating_a_public_url(): void
83 | {
84 | $url = $this->adapter()->publicUrl('path.txt', new Config());
85 |
86 | self::assertEquals('https://example.org.local/assets/path_prefix_12345/path.txt', $url);
87 | }
88 |
89 | public function overwriting_a_file(): void
90 | {
91 | $this->runScenario(function () {
92 | $this->givenWeHaveAnExistingFile('path.txt', 'contents', ['visibility' => Visibility::PUBLIC]);
93 | $adapter = $this->adapter();
94 |
95 | $adapter->write('path.txt', 'new contents', new Config(['visibility' => Visibility::PRIVATE]));
96 |
97 | $contents = $adapter->read('path.txt');
98 | $this->assertEquals('new contents', $contents);
99 | // $visibility = $adapter->visibility('path.txt')->visibility();
100 | // $this->assertEquals(Visibility::PRIVATE, $visibility); // Commented out of this test
101 | });
102 | }
103 |
104 | /**
105 | * @test
106 | */
107 | public function get_checksum(): void
108 | {
109 | $adapter = $this->adapter();
110 |
111 | if (! $adapter instanceof ChecksumProvider) {
112 | $this->markTestSkipped('Adapter does not supply providing checksums');
113 | }
114 |
115 | $adapter->write('path.txt', 'foobar', new Config());
116 |
117 | $this->assertSame(
118 | '3858f62230ac3c915f300c664312c63f',
119 | $adapter->checksum('path.txt', new Config())
120 | );
121 |
122 | $this->assertSame(
123 | 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2',
124 | $adapter->checksum('path.txt', new Config(['checksum_algo' => 'sha256']))
125 | );
126 | }
127 |
128 | public function test_construct_throws_error(): void
129 | {
130 | $this->expectException(\RuntimeException::class);
131 | $this->expectExceptionMessage('PrefixPath is no longer supported directly. Use PathPrefixedAdapter instead: https://flysystem.thephpleague.com/docs/adapter/path-prefixing/');
132 | new BunnyCDNAdapter(self::bunnyCDNClient(), 'https://example.org.local/assets/', 'thisisauselessarg');
133 | }
134 |
135 | /**
136 | * @test
137 | */
138 | public function prefix_path(): void
139 | {
140 | $this->runScenario(function () {
141 | $regularAdapter = self::bunnyCDNAdapter();
142 | $prefixPathAdapter = new PathPrefixedAdapter($regularAdapter, self::PREFIX_PATH);
143 |
144 | self::assertNotEmpty(
145 | self::PREFIX_PATH
146 | );
147 |
148 | self::assertIsString(
149 | self::PREFIX_PATH
150 | );
151 |
152 | $content = 'this is test';
153 | $prefixPathAdapter->write(
154 | 'source.file.svg',
155 | $content,
156 | new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])
157 | );
158 |
159 | self::assertTrue($prefixPathAdapter->fileExists(
160 | 'source.file.svg'
161 | ));
162 |
163 | self::assertTrue($regularAdapter->directoryExists(
164 | self::PREFIX_PATH
165 | ));
166 |
167 | self::assertTrue($regularAdapter->fileExists(
168 | self::PREFIX_PATH.'/source.file.svg'
169 | ));
170 |
171 | $prefixPathAdapter->copy(
172 | 'source.file.svg',
173 | 'source.copy.file.svg',
174 | new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])
175 | );
176 |
177 | self::assertTrue($regularAdapter->fileExists(
178 | self::PREFIX_PATH.'/source.copy.file.svg'
179 | ));
180 |
181 | self::assertTrue($prefixPathAdapter->fileExists(
182 | 'source.copy.file.svg'
183 | ));
184 |
185 | $prefixPathAdapter->delete(
186 | 'source.copy.file.svg'
187 | );
188 |
189 | $this->assertEquals($content, $prefixPathAdapter->read('source.file.svg'));
190 |
191 | $this->assertEquals(
192 | $prefixPathAdapter->read('source.file.svg'),
193 | $regularAdapter->read(self::PREFIX_PATH.'/source.file.svg')
194 | );
195 |
196 | $this->assertEquals($content, stream_get_contents($prefixPathAdapter->readStream('source.file.svg')));
197 |
198 | $this->assertEquals(
199 | stream_get_contents($prefixPathAdapter->readStream('source.file.svg')),
200 | stream_get_contents($regularAdapter->readStream(self::PREFIX_PATH.'/source.file.svg'))
201 | );
202 |
203 | $this->assertSame(
204 | 'image/svg+xml',
205 | $prefixPathAdapter->mimeType('source.file.svg')->mimeType()
206 | );
207 |
208 | $this->assertEquals(
209 | $prefixPathAdapter->mimeType('source.file.svg')->mimeType(),
210 | $regularAdapter->mimeType(self::PREFIX_PATH.'/source.file.svg')->mimeType()
211 | );
212 |
213 | $this->assertGreaterThan(
214 | 0,
215 | $prefixPathAdapter->fileSize('source.file.svg')->fileSize()
216 | );
217 |
218 | $this->assertEquals(
219 | $prefixPathAdapter->fileSize('source.file.svg')->fileSize(),
220 | $regularAdapter->fileSize(self::PREFIX_PATH.'/source.file.svg')->fileSize()
221 | );
222 |
223 | $this->assertGreaterThan(
224 | time() - 30,
225 | $prefixPathAdapter->lastModified('source.file.svg')->lastModified()
226 | );
227 |
228 | $this->assertEquals(
229 | $prefixPathAdapter->lastModified('source.file.svg')->lastModified(),
230 | $regularAdapter->lastModified(self::PREFIX_PATH.'/source.file.svg')->lastModified()
231 | );
232 |
233 | $prefixPathAdapter->delete(
234 | 'source.file.svg'
235 | );
236 |
237 | self::assertFalse($prefixPathAdapter->fileExists(
238 | 'source.file.svg'
239 | ));
240 |
241 | $prefixPathAdapter->write(
242 | 'subfolder/subfolder2/source.file.svg',
243 | $content,
244 | new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])
245 | );
246 |
247 | self::assertTrue($regularAdapter->fileExists(
248 | self::PREFIX_PATH.'/subfolder/subfolder2/source.file.svg'
249 | ));
250 |
251 | $prefixPathAdapter->move(
252 | 'subfolder',
253 | 'newsubfolder',
254 | new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])
255 | );
256 |
257 | self::assertFalse($regularAdapter->fileExists(
258 | self::PREFIX_PATH.'/subfolder/subfolder2/source.file.svg'
259 | ));
260 |
261 | self::assertTrue($prefixPathAdapter->fileExists(
262 | 'newsubfolder/subfolder2/source.file.svg'
263 | ));
264 | });
265 | }
266 |
267 | /**
268 | * @test
269 | *
270 | * @throws FilesystemException
271 | * @throws \Throwable
272 | */
273 | public function prefix_path_not_in_meta_pr_36(): void
274 | {
275 | $this->dontRetryOnException();
276 |
277 | $this->runScenario(function () {
278 | $prefixPathAdapter = $this->adapter();
279 |
280 | $prefixPathAdapter->write(
281 | 'source.file.svg',
282 | '----',
283 | new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])
284 | );
285 |
286 | $contents = \iterator_to_array($prefixPathAdapter->listContents('/', false));
287 |
288 | $this->assertCount(1, $contents);
289 | $this->assertSame('source.file.svg', $contents[0]['path']);
290 |
291 | $prefixPathAdapter->delete('source.file.svg');
292 | });
293 | }
294 | }
295 |
--------------------------------------------------------------------------------
/tests/UtilityClassTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(
19 | Util::startsWith('/test', '/')
20 | );
21 |
22 | $this->assertFalse(
23 | Util::startsWith('test', '/')
24 | );
25 | }
26 |
27 | /**
28 | * @test
29 | *
30 | * @throws Exception
31 | */
32 | public function it_ends_with()
33 | {
34 | $this->assertTrue(
35 | Util::endsWith('test/', '/')
36 | );
37 |
38 | $this->assertFalse(
39 | Util::endsWith('test', '/')
40 | );
41 |
42 | $this->assertTrue(
43 | Util::endsWith('test', '')
44 | );
45 | }
46 |
47 | /**
48 | * @test
49 | *
50 | * @throws Exception
51 | */
52 | public function it_tests_normalize_path()
53 | {
54 | $this->assertEquals(
55 | 'test/',
56 | Util::normalizePath('/test/', true)
57 | );
58 |
59 | $this->assertEquals(
60 | 'test/',
61 | Util::normalizePath('/test', true)
62 | );
63 |
64 | $this->assertEquals(
65 | 'test',
66 | Util::normalizePath('/test', false)
67 | );
68 | }
69 |
70 | /**
71 | * @test
72 | *
73 | * @throws Exception
74 | */
75 | public function it_path_split()
76 | {
77 | $this->assertEquals(
78 | [
79 | 'file' => 'testing-dir',
80 | 'dir' => '',
81 | ],
82 | Util::splitPathIntoDirectoryAndFile('/testing-dir')
83 | );
84 |
85 | $this->assertEquals(
86 | [
87 | 'file' => 'testing.txt',
88 | 'dir' => '',
89 | ],
90 | Util::splitPathIntoDirectoryAndFile('/testing.txt')
91 | );
92 |
93 | $this->assertEquals(
94 | [
95 | 'file' => 'testing-dir',
96 | 'dir' => '',
97 | ],
98 | Util::splitPathIntoDirectoryAndFile('/testing-dir/')
99 | );
100 |
101 | $this->assertEquals(
102 | [
103 | 'file' => 'file.txt',
104 | 'dir' => '/testing-dir',
105 | ],
106 | Util::splitPathIntoDirectoryAndFile('/testing-dir/file.txt')
107 | );
108 |
109 | $this->assertEquals(
110 | [
111 | 'file' => 'file.txt',
112 | 'dir' => '/testing-dir/nested',
113 | ],
114 | Util::splitPathIntoDirectoryAndFile('/testing-dir/nested/file.txt')
115 | );
116 | }
117 |
118 | public function test_replace_first()
119 | {
120 | $this->assertSame(
121 | 'SX',
122 | Util::replaceFirst('X', 'S', 'XX')
123 | );
124 |
125 | $this->assertSame(
126 | 'ORIGINAL',
127 | Util::replaceFirst('X', 'S', 'ORIGINAL')
128 | );
129 | }
130 | }
131 |
--------------------------------------------------------------------------------