[
  {
    "path": ".dockerignore",
    "content": ".git\n"
  },
  {
    "path": ".editorconfig",
    "content": "[*]\nindent_style = space\n\n[*.yml]\nindent_size = 2\n\n[composer.json]\nindent_size = 4\n\n[*.php]\nend_of_line = lf\ninsert_final_newline = true\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto\n\n/.docker export-ignore\n/.dockerignore export-ignore\n/docker-compose.yml export-ignore\n/config.subsplit-publish.json export-ignore\n/.editorconfig export-ignore\n/.php-cs-fixer.dist.php export-ignore\n/phpstan.neon export-ignore\n/phpstan-baseline.neon export-ignore\n/phpunit.php export-ignore\n/phpunit.xml.dist export-ignore\n/.travis.yml export-ignore\n/.scrutinizer.yml export-ignore\n/CHANGELOG.md export-ignore\n/CODE_OF_CONDUCT.md export-ignore\n/deprecations.md export-ignore\n/docker-composer.yml export-ignore\n/README.md export-ignore\n/CODE_OF_CONDUCT.md export-ignore\n/.github export-ignore\n/src/AsyncAwsS3 export-ignore\n/src/AwsS3V3 export-ignore\n/src/GoogleCloudStorage export-ignore\n/src/Ftp export-ignore\n/src/InMemory export-ignore\n/src/PhpseclibV2 export-ignore\n/src/PhpseclibV3 export-ignore\n/src/AdapterTestUtilities export-ignore\n/src/AzureBlobStorage export-ignore\n/src/ZipArchive export-ignore\n/src/WebDAV export-ignore\n/src/PathPrefixing export-ignore\n/src/Local export-ignore\n/src/ReadOnly export-ignore\n/src/GridFS export-ignore\n/.gitattributes export-ignore\n/.gitignore export-ignore\n/bin/ export-ignore\n/mocked-functions.php export-ignore\n/test_files/ export-ignore\n/**/*Test.php export-ignore\n/**/*Stub.php export-ignore\n/**/Stub*.php export-ignore\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Bug.md",
    "content": "---\nname: 🐛 Bug\nabout: Did you encounter a bug?\n---\n\n### Bug Report\n\n<!-- Fill in the relevant information below to help triage your issue. -->\n\n| Q                 | A       |\n|-------------------|---------|\n| Flysystem Version | x.y.z   |\n| Adapter Name      | example |\n| Adapter version   | x.y.z   |\n\n#### Summary\n\n<!-- Provide a summary describing the problem you are experiencing. -->\n\n#### How to reproduce\n\n<!--\nProvide steps to reproduce the issue.\nIf possible, also add a code snippet.\n-->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Feature_Request.md",
    "content": "---\nname: 🎉 Feature Request\nabout: Do you have a new feature in mind?\n---\n\n### Feature Request\n\n<!-- Fill in the relevant information below to help triage your issue. -->\n\n| Q                 | A       |\n|-------------------|---------|\n| Flysystem Version | x.y.z   |\n| Adapter Name      | example |\n| Adapter version   | x.y.z   |\n\n#### Scenario / Use-case\n\n<!-- Provide an explain in which scenario the feature would be helpful. --> \n\n#### Summary\n\n<!-- Provide a summary of the feature you would like to see implemented. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Question.md",
    "content": "---\nname: ❓ Question\nabout: Are you unsure about something?\n---\n\n### Question\n\n<!-- Fill in the relevant information below to help triage your issue. -->\n\n| Q                 | A       |\n|-------------------|---------|\n| Flysystem Version | x.y.z   |\n| Adapter Name      | example |\n| Adapter version   | x.y.z   |\n"
  },
  {
    "path": ".github/release.yml",
    "content": "changelog:\n  categories:\n    - title: Breaking Changes 🛠\n      labels:\n        - Semver-Major\n        - breaking-change\n    - title: Exciting New Features 🎉\n      labels:\n        - Semver-Minor\n        - enhancement\n    - title: Other Changes\n      labels:\n        - \"*\""
  },
  {
    "path": ".github/stale.yml",
    "content": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 60\n# Number of days of inactivity before a stale issue is closed\ndaysUntilClose: 7\n# Issues with these labels will never be considered stale\nexemptLabels:\n    - keep open\n    - 2.0 ideas\n# Label to use when marking an issue as stale\nstaleLabel: stale\n# Comment to post when marking an issue as stale. Set to `false` to disable\nmarkComment: >\n    This issue has been automatically marked as stale because it has not had\n    recent activity. It will be closed if no further activity occurs. Thank you\n    for your contributions.\n# Comment to post when closing a stale issue. Set to `false` to disable\ncloseComment: false\n"
  },
  {
    "path": ".github/workflows/publish-subsplits.yml",
    "content": "---\nname: Sub-Split Publishing\non:\n  push:\n    branches:\n      - 3.x\n  create:\n    tags:\n      - '3.*'\n  delete:\n    tags:\n      - '3.*'\n\njobs:\n  publish_subsplits:\n    runs-on: ubuntu-latest\n    name: Publish package sub-splits\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: '0'\n          persist-credentials: 'false'\n      - uses: frankdejonge/use-github-token@1.1.0\n        with:\n          authentication: 'frankdejonge:${{ secrets.PERSONAL_ACCESS_TOKEN }}'\n          user_name: 'Frank de Jonge'\n          user_email: 'info@frenky.net'\n      - name: Cache splitsh-lite\n        id: splitsh-cache\n        uses: actions/cache@v4\n        with:\n          path: './.splitsh'\n          key: '${{ runner.os }}-splitsh'\n      - uses: frankdejonge/use-subsplit-publish@1.1.0\n        with:\n          source-branch: '3.x'\n          config-path: './config.subsplit-publish.json'\n          splitsh-path: './.splitsh/splitsh-lite'\n          splitsh-version: 'v1.0.1'\n"
  },
  {
    "path": ".github/workflows/quality-assurance.yml",
    "content": "---\nname: Quality Assurance\nconcurrency:\n  group: ${{ github.ref }}\n  cancel-in-progress: true\non:\n  push:\n    paths:\n      - src/**/*.php\n      - .github/workflows/quality-assurance.yml\n    branches:\n      - 3.x\n      - 4.x\n  pull_request:\n    paths:\n      - src/**/*.php\n      - .github/workflows/quality-assurance.yml\n    branches:\n      - 3.x\n      - 4.x\n  schedule:\n    - cron: \"5 1 * * *\"\n\nenv:\n  FLYSYSTEM_AWS_S3_KEY: '${{ secrets.FLYSYSTEM_AWS_S3_KEY }}'\n  FLYSYSTEM_AWS_S3_SECRET: '${{ secrets.FLYSYSTEM_AWS_S3_SECRET }}'\n  FLYSYSTEM_AWS_S3_BUCKET: '${{ secrets.FLYSYSTEM_AWS_S3_BUCKET }}'\n  MONGODB_URI: 'mongodb://127.0.0.1:27017/'\n  FLYSYSTEM_TEST_DANGEROUS_THINGS: \"yes\"\n  FLYSYSTEM_TEST_SFTP: \"yes\"\n\njobs:\n  phpunit:\n    name: PHPUnit tests on ${{ matrix.php }} ${{ matrix.composer-flags }}\n    runs-on: ubuntu-latest\n    continue-on-error: ${{ matrix.experimental }}\n    strategy:\n      fail-fast: false\n      matrix:\n        php: [ '8.2', '8.3', '8.4', '8.5' ]\n        composer-flags: [ '' ]\n        experimental: [false]\n        phpstan: [true]\n        phpunit-flags: [ '--coverage-text' ]\n#        include:\n#          - php: '8.2'\n#            experimental: false\n#            phpstan: false\n#            phpunit-flags: '--no-coverage'\n#          - php: '8.3'\n#            experimental: false\n#            phpstan: false\n#            phpunit-flags: '--no-coverage'\n#          - php: '8.4'\n#            experimental: false\n#            phpstan: false\n#            phpunit-flags: '--no-coverage'\n    steps:\n      - uses: actions/checkout@v4\n      - run: docker compose -f docker-compose.yml up -d\n      - name: Start an SSH Agent\n        uses: frankdejonge/use-ssh-agent@1.1.0\n      - run: chmod 0400 ./test_files/sftp/id_*\n      - id: ssh_agent\n        run: ssh-add ./test_files/sftp/id_rsa\n      - uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php }}\n          extensions: mongodb\n          coverage: pcov\n          tools: composer:v2\n      - run: composer update --no-progress ${{ matrix.composer-flags }}\n      - run: php test_files/wait_for_sftp.php\n      - run: php test_files/wait_for_ftp.php 2121\n      - run: php test_files/wait_for_ftp.php 2122\n      - run: COMPOSER_OPTS='${{ matrix.composer-flags }}' vendor/bin/phpunit ${{ matrix.phpunit-flags }}\n      - run: vendor/bin/phpstan analyse\n        if: ${{ matrix.phpstan }}\n      - run: vendor/bin/php-cs-fixer fix --diff --dry-run\n        continue-on-error: true\n        if: ${{ matrix.php == '8.0' }}\n\n"
  },
  {
    "path": ".github/workflows/set-subsplit-default-branch.yml",
    "content": "---\nname: Update default sub-split branch\n\non: workflow_dispatch\n\njobs:\n  set-default-branch:\n    name: Set default git branch\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: '0'\n          persist-credentials: 'false'\n      - uses: actions/github-script@v7\n        with:\n          github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}\n          script: |\n            const fs = require('fs');\n            let splits = JSON.parse(fs.readFileSync('config.subsplit-publish.json'))['sub-splits'];\n\n            for (let split of splits) {\n              const { groups: { repo } } = /git@github\\.com:thephpleague\\/(?<repo>.*)\\.git/.exec(split.target);\n              console.log(`Found repo ${repo}`);\n              await github.rest.repos.update({\n                owner: 'thephpleague',\n                repo,\n                default_branch: '3.x',\n              });\n            }\n"
  },
  {
    "path": ".gitignore",
    "content": "/vendor/\n/coverage/\n/.phpunit.result.cache\n/phpunit.xml\n/composer.lock\n/index_*.php\n/.php-cs-fixer.php\n/.php-cs-fixer.cache\n/google-cloud-service-account.json\n.idea"
  },
  {
    "path": ".php-cs-fixer.dist.php",
    "content": "<?php\n\n$finder = PhpCsFixer\\Finder::create()\n    ->in([__DIR__ . '/'])\n    ->exclude(__DIR__ . '/vendor');\n\nreturn (new PhpCsFixer\\Config())\n    ->setRules([\n        '@PSR2' => true,\n        'array_syntax' => ['syntax' => 'short'],\n        'binary_operator_spaces' => true,\n        'single_line_after_imports' => true,\n        'blank_line_before_statement' => ['statements' => ['return']],\n        'cast_spaces' => true,\n        'concat_space' => ['spacing' => 'one'],\n        'no_singleline_whitespace_before_semicolons' => true,\n        'not_operator_with_space' => true,\n        'no_unused_imports' => true,\n        'phpdoc_align' => false,\n        'phpdoc_indent' => true,\n        'phpdoc_no_access' => true,\n        'phpdoc_no_alias_tag' => true,\n        'phpdoc_no_package' => true,\n        'phpdoc_scalar' => true,\n        'phpdoc_separation' => true,\n        'phpdoc_summary' => true,\n        'phpdoc_to_comment' => true,\n        'phpdoc_trim' => true,\n        'single_blank_line_at_eof' => true,\n        'ternary_operator_spaces' => true,\n        'ordered_imports' => [\n            'sort_algorithm' => 'alpha',\n            'imports_order' => ['const', 'class', 'function'],\n        ],\n        'no_extra_blank_lines' => true,\n        'no_whitespace_in_blank_line' => true,\n        'nullable_type_declaration_for_default_null_value' => true,\n    ])\n    ->setFinder($finder);\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## 3.32.0 - 2026-02-25\n\n### Changes\n\n- [AwsS3V3] Allow SSE-C options when fetching file metadata\n\n## 3.31.0 - 2026-01-23\n\n### Changes\n\n- [AsyncAwsS3] Allow V3\n\n## 3.30.2 - 2025-11-10\n\n### Fixes\n\n- [Local] Clear last error for rename and copy operations (#1883) \n\n## 3.30.1 - 2025-10-20\n\n### Fixes\n\n- Ensure listing directories called \"0\" do not produce unfiltered listings.\n\n## 3.30.0 - 2025-06-25\n\n### Changes\n\n- [MongoDB] Allow v2\n- [AsyncAWS] Encode/decode object identifiers\n- [GoogleCloudStorage] Add option to stream responses\n- [Local] Clear stat cache consistently\n- [SFTP] Add option to disconnect connections on destruction\n- [WebDAV] Deal with 405 case when trying to create a directory that already exists.\n\n## 3.29.1 - 2024-10-08\n\n### Fixes\n\n- Normalise path for checksum call\n\n## 3.29.0 - 2024-09-29\n\n### Changes\n\n- [FTP] Add error context from error messages\n- [SFTP] overwrite on move\n- [AWS S3] same path copy/move no-op\n- [Async S3] transform error to flysystem errors\n\n## 3.28.0 - 2024-05-22\n\n### Added\n\n- MongoDB GridFS adapter (by @GromNaN)\n\n### Fixed\n\n- PHP 8.3 directory listing issue for the FTP adapter (by @lanz1)\n\n## 3.27.0 - 2024-04-07\n\n### Fixed\n\n- Corrected AWS SSE-C Options\n- Handle MetadataDirective gracefully.\n\n## 3.26.0 - 2024-03-25\n\n### Fixed\n\n- Make SFTP connectivity pinging an opt-in feature.\n\n### Added \n\n- Add `add_content_md5` option to AWS S3 (#1774)\n- Added AWS SSE-C options (#1773)\n\n## 3.25.1 - 2024-03-16\n\n### Fixed\n\n- Cleanup connection instance after disconnecting SFTP connection.\n- Fix upcoming PHP 8.4 deprecations (#1772) \n\n## 3.25.0 - 2024-03-09\n\n### Added\n\n- [MountManager] added ability to (dangerously) mount additional filesystems\n- [FTP] added `disconnect` method to proactively close connections\n- [SFTP V3] added `disconnect` method to proactively close connections\n\n## 3.24.0 - 2024-02-04\n\n### Fixes\n\n- Updated method signatures to match upgraded dependency signatures for overrides (#1748, #1746)\n- Added missing path prefixing in FTP implementation (#1747)\n\n### Changes\n\n- Updated string assertions to use PHP 8 functions (#1750, #1749))\n\n## 3.23.1 - 2024-01-26\n\n### Changes\n\n- Updated license year\n\n## 3.23.0 - 2023-12-04\n\n### Fixed\n\n- Fixed upstream regression caused by resolving inconclusive mime-type.\n\n### Added\n\n- Made inconclusive mime-type resolving configurable on the local adapter.\n\n## 3.22.0 - 2023-12-03\n\n### Changes\n\n- Prevent double directory creation with lazy root creation for Local filesystem.\n\n### Fixes\n\n- Resolve to \"inconclusive\" mimetype instead of causing a type error by @GuySartorelli\n- Corrected spelling of a configuration key for the Azure adapter by @shineability\n\n### Additions\n\n- MountManager::extend allows for immutable dynamic extension of the mount manager.\n- Added a new abstract DecoratedAdapter for easier decoration of adapters by @jnoordsij\n\n## 3.21.0 - 2023-11-18\n\n### Changes\n\n- Retain visibility on local copy for local FS, in line with other adapter (#1730) by @jnoordsij\n\n## 3.20.0 - 2023-11-14\n\n### Changed\n\n- Normalise paths for public and temporary URLs (#1727)\n\n## 3.19.0 - 2023-11-07\n\n### Added\n\n- Configuration option to specify if visibility should be retained during copy/move operations\n- InMemoryFilesystemAdapter now supports visibility changes on move and copy.\n- Default visibility options are ignored when moving/copying while respecting visibility retention settings.\n- Local filesystem implementation now allows setting visibility on move and copy.\n\n## 3.18.0 - 2023-10-05\n\n### Added\n\n- Configuration option to specify how to handle same path copy/move operations (#1715)\n\n## 3.17.0 - 2023-10-05\n\n### Added\n\n- [AsyncAWS] Added support for version 2.0 of async-aws/{s3,simple-s3}\n\n## 3.16.0 - 2023-09-07\n\n### Added\n\n- [AsyncAws] Allow specifying `get_object_options` for temporary URL generation\n\n### Fixed\n\n- [ZipArchive] override on move\n- [WebDAV] encode path for propfind actions\n- [PathPrefixing]  [#1686](https://github.com/thephpleague/flysystem/issues/1686)\n\n## 3.15.1 - 2023-05-04\n\n### Fixed\n\n- Remove duplicate class caused by package extractin and inclusion\n\n## 3.15.0 - 2023-05-04\n\n### Added\n\n- Extracted the local adapter as a standalone package\n\n### Changed\n\n- Removed readme's from shipped artefacts.\n\n## 3.14.0 - 2023-04-11\n\n### Added\n\n- Made disabling stat cache configurable for SFTP adapters.\n\n## 3.13.0 - 2023-04-11\n\n### Fixed\n\n- AsyncAwsS3 object deletion now chunks per 100 objects to prevent memory exhaustion\n- AsyncAwsS3 now disregards top-level directories from listings\n- LocalAdapter now deals with file deletions during directory listings gracefully.\n\n### Added\n\n- DirectoryListing now supports correct phpstan for map and filter methods. \n- FTP/SFTP added config option to detect the mime-type using the path alone (prevents a read)\n- SFTP now supports PuTTY style private keys\n- \n\n## 3.12.3 - 2023-02-18\n\n### Fixed\n\n- [Google Cloud Storage] Fixed ACL error for uniform bucker ACL copy operations.\n- \n- ## 3.12.2 - 2023-01-19\n\n### Fixed\n\n- [AWS S3] Corrected param order for doesObjectExistV2 call\n\n## 3.12.1 - 2023-01-06\n\n### Fixed\n\n- [AWS S3] Use doesObjectExistV2 to prevent false positive respomnses.\n\n## 3.12.0 - 2022-12-20\n\n### Added\n\n- [Core] Chained public URL generation strategy\n\n### Fixed\n\n- [WebDAV] Handle cases where the content listing returns entries with URL prefixes.\n- [Local] Ensure correct implicit root creations happens on windows.\n- [ZipArchive] Fix incorrect zip contents listing for top-level directory.\n\n## 3.11.0 - 2022-12-02\n\n### Added\n\n- [Google Cloud Storage] Added `UniformBucketLevelAccessVisibility` to allow buckets with uniform bucket-level access policies.\n\n## 3.10.4 - 2022-11-26\n\n### Changed\n\n- [I became a dad, meet Tim](https://twitter.com/frankdejonge/status/1594966175108177921)\n\n### Fixed\n\n- [PathPrefixing] ensure `checksum` and `temporaryUrl` are prefixed\n- [WebDAV] ensure directory creation uses trailing slashes for paths\n\n## 3.10.3 - 2022-11-14\n\n### Fixed\n\n- [Local] Handle checksum errors without message (#1590)\n\n## 3.10.2 - 2022-10-25\n\n### Fixed\n\n- [Filesystem] Ensure adapter is used for exposing temporary URLs.\n\n## 3.10.1 - 2022-10-21\n\n### Fixed\n\n- [Filesystem] Added missing constructor argument to allow temporary URL generator injection.\n\n## 3.10.0 - 2022-10-21\n\n### Added\n\n- [Filesystem] added `temporaryUrl` method\n- [AsyncAWS] added `temporaryUrl` method\n- [AWS S3] added `temporaryUrl` method\n- [Azure Blob Storage] added `temporaryUrl` method\n- [MountManager] added `temporaryUrl` method\n- [Google Cloud Storage] added `temporaryUrl` method\n- [ReadOnly] added `temporaryUrl` method\n- [PathPrefixing] added `temporaryUrl` method\n\n## 3.9.0 - 2022-10-18\n\n### Added\n\n- [Filesystem] Added ability to inject custom public URL generator into a filesystem.\n- [MountManager] added `checksum` and `publicUrl` methods\n- [ZipArchive] Do not prefix directories when creating/reading an archive\n- [ShardedPrefixPublicUrlGenerator] added url generator strategy that distributes over a list of prefixes\n\n## 3.8.0 - 2022-10-18\n\n### Added\n\n- [ChecksumAlgoIsNotSupported] Exception to indicate a checksum is not supported by the checksum provider, filesystem will fall back to ad-hoc generation.\n\n## 3.7.0 - 2022-10-17\n\n### Added\n\n- [Filesystem] added `checksum` method\n- [AWS S3] added `checksum` method\n- [Async S3] added `checksum` method\n- [Google Cloud Storage] added `checksum` method\n- [Azure Blob Storage] added `checksum` method\n\n## 3.6.0 - 2022-10-13\n\n### Added\n\n- [Filesystem] Added public url method\n- [Azure Blob Storage] Added public url method\n- [AWS S3] Added public url method\n- [Async S3] Added public url method\n- [GCS] Added public url method\n- [WebDAV] Added public url method\n- [ReadOnly] Added public url method\n- [PathPrefixing] Added public url method\n\n## 3.5.3 - 2022-09-23\n\n### Fixed\n\n- [SFTP] Account for missing \"type\" field in metadata result.\n\n## 3.5.2 - 2022-09-23\n\n### Fixed\n\n- [SFTP v2/v3] Fixed possible race-condition during directory creation leading to false failures.\n\n## 3.5.1 - 2022-09-18\n\n### Fixed\n\n- [WebDAV] Strip directory prefix in `createDirectory` to prevent double prefixing in `directoryExists`.\n\n## 3.5.0 - 2022-09-17\n\n### Added\n\n- [AWS S3] Allow specifying visibility on move and copy\n\n## 3.4.0 - 2022-09-15\n\n### Added\n\n- Added FTP configuration option useRawListOptions (null|false|true).\n- UnableToListContents exception was added to uniformly represent content listing exceptions.\n\n### Fixed\n\n- [FTP] Don't use raw list options for FileZilla FTP servers ([#1553](https://github.com/thephpleague/flysystem/pull/1553))\n- [WebDAV] Correct path formatting for move and copy operations ([#1552](https://github.com/thephpleague/flysystem/pull/1552))\n\n## 3.3.0 - 2022-09-09\n\n### Added\n\n- StaticInMemoryAdapterRegistry contributed by @kbond\n- ReadonlyFilesystemAdapter contributed by @kbond\n- PathPrefixedAdapter contributed by @shyim\n\n### Fixed\n\n- WebDAV prefix is now encoded and the dir is not required to be pre-created ([#1533](https://github.com/thephpleague/flysystem/pull/1533))\n\n## 3.2.1 - 2022-08-14\n\n### Fixed\n\n- [ZipArchive] reverted regression introduced in [#1525](https://github.com/thephpleague/flysystem/pull/1525)\n\n## 3.2.0 - 2022-07-26\n\n### Added\n\n- [AwsS3V3] Added configuration options for forwarded options, multipart upload configuration, and metadata fields.\n\n### Fixes\n\n- [ZipArchive] delete top-level directory when deleting directories.\n- [AwsS3V3] add `ChecksumAlgorithm` to forwarded options.\n- [AwsS3V3] add `ContentMD5` to forwarded options.\n- [AwsS3V3] made forwarded options and metadata fields configurable.\n- [SftpV3] upgrade minimum version, PHP 8 and the lowest version fails to authenticate.\n\n## 3.1.1 - 2022-07-18\n\n- [AwsS3V3] Corrected exception type (#1524)\n\n## 3.1.0 - 2022-06-29\n\n- Added option for the Local adapter to create the root directory only on the first mutating (write/copy/move) action.\n\n## 3.0.23 - 2022-06-29\n\n- Added reasons for exceptions for all adapters that were missing previous exception messages.\n\n## 3.0.22 - 2022-06-29\n\n- [AwsS3V3] Added reasons for exceptions\n- [AwsS3V3] Use ListObjectsV2 instead of ListObjects\n\n## 3.0.21 - 2022-06-12\n\n- [AwsS3V3] Use ListObjectsV2 instead of ListObjects\n\n## 3.0.20 - 2022-05-25\n\n### Fixed\n\n- [Core] Fix deprecated ${var} string interpolation patterns (#1470)\n\n## 3.0.19 - 2022-05-03\n\n### Fixed\n\n- [FTP] Turn errors into proper exceptions when resolving the connection root (#1460)\n\n## 3.0.18 - 2022-04-25\n\n### Fixed\n\n- [SFTP v3] Fix retries (#1451)\n\n## 3.0.17 - 2022-04-14\n\n### Fixed\n\n- [SFTP v2] Avoid type errors when public key is not retrieved (#1446)\n- [SFTP v3] Avoid type errors when public key is not retrieved (#1446)\n\n## 3.0.16 - 2022-04-11\n\n### Fixed\n\n- [Local] fall back to extension lookups when the mime-type comes up as inconclusive.\n\n## 3.0.15 - 2022-04-08\n\n### Fixed\n\n- [GCS] Allow setting upload metadata\n- [GCS] Allow setting contentType, or resolve it\n- [SFTP v2+v3] Delete top-level directory too.\n\n## 3.0.14 - 2022-04-06\n\n### Added\n\n- [InMemory] allow to set a last-updated time (#1438)\n- [SFTP V3] allow configuring preferred algo's (#1440)\n\n## 3.0.13 - 2022-04-02\n\n### Fixed\n\n- [AWS S3 V3] Do not return top-level directory when listing that same directory\n\n## 3.0.12 - 2022-03-12\n\n### Fixed\n\n- [SFTP V3] Fix issue where listing is false.\n- [Async AWS S3] Cosmetic fix for directory prefixing.\n\n## 3.0.11 - 2022-03-04\n\n### Fixed\n\n- [AWS S3] Use globally configured options.\n\n## 3.0.10 - 2022-02-26\n\n### Fixed\n\n- [AWS S3] fix detecting directories that only contain other directories but no files.\n- [AWS S3] when checking for directory existence, limit the result set (perf)\n- [AWS S3] throw interface exception when failing to delete directory\n- [Async AWS S3] when checking for directory existence, limit the result set (perf)\n\n## 3.0.9 - 2022-02-22\n\n### Fixed\n\n- [AWS S3] support setting an ACL as a direct option instead of using visibility.\n\n## 3.0.8 - 2022-02-16\n\n### Fixed\n\n- [AWS S3] Set ContentType when mime-type config option is set during writes, like in v1.\n\n## 3.0.7 - 2022-02-14\n\n### Fixed\n\n- [WebDAV] added missing composer.json for sub-split\n\n## 3.0.6 - 2022-02-14\n\n### Added\n\n- [WebDAV] new adapter added\n\n### Fixed\n\n- [Core] Trim slashed uniformly in the attribute classes.\n- [Core] Uniformly use directory_visibility over visibility for directory usage.\n- [FTP] export-ignore the test case.\n\n## 3.0.5 - 2022-02-12\n\n### Added\n\n- [AzureBlobStorage] New adapter added\n\n### Fixed\n\n- [AsyncAwsS3] Make EXTRA_METADATA_FIELDS protected to prevent error when extending the class\n\n## 3.0.4 - 2022-02-10\n\n### Fixed\n\n- [FTP] Do not require setting of the root directory, use '' by default.\n\n## 3.0.3 - 2022-01-31\n\n### Fixed\n\n- [FTP] Made connection resolving lazy again (#1414)\n\n## 3.0.2 - 2022-01-30\n\n### Fixes\n\n* [FTP] Support relative or empty connection root directories (#1410)\n\n## 3.0.1 - 2022-01-15\n\n### Fixes\n\n* [ZipArchive] delete top-level directory too when deleting a directory\n* [GoogleCloudStorage] Use listing to check for directory existence (consistency)\n* [GoogleCloudStorage] Fixed bug where exceptions were not thrown \n* [AwsS3V3] Allow passing options for controlling multi-upload options (#1396)\n* [Local] Convert windows-style directory separator to unix-style (#1398)\n\n## 3.0.0 - 2022-01-13\n\n### Added\n\n* FilesystemReader::has to check for directory or file existence\n* FilesystemReader::directoryExists to check for directory existence\n* FilesystemReader::fileExists to check for file existence\n* FilesystemAdapter::directoryExists to check for directory existence\n* FilesystemAdapter::fileExists to check for file existence\n\n## 2.5.0 - 2022-09-17\n\n### Added\n\n- [AWS S3] Allow specifying visibility on move and copy\n\n## 2.4.5 - 2022-04-25\n\n- [SFTP v3] Fix retries (#1451)\n\n## 2.4.4 - 2022-04-14\n\n### Fixed\n\n- [SFTP v2] Avoid type errors when public key is not retrieved (#1446)\n- [SFTP v3] Avoid type errors when public key is not retrieved (#1446)\n\n## 2.4.3 - 2022-02-16\n\n### Fixed\n\n- [AWS S3] Set ContentType when mime-type config option is set during writes, like in v1.\n\n## 2.4.2 - 2022-01-31\n\n### Fixed\n\n- [FTP] Made connection resolving lazy again (#1414)\n\n## 2.4.1 - 2022-01-30\n\n### Fixed\n\n- [FTP] Fix relative connection root handling\n\n## 2.4.0 - 2022-01-04\n\n### Added\n\n- [SFTP V3] New adapter officially published\n\n## 2.3.2 - 2021-11-28\n\n### Fixed\n\n- [FTP] Check for FTP\\Connection object in addition to a `resource` for connectivity checks and connection handling.\n- [Local] Simplify writeStream, as a bonus, have an EXT_LOCK on it now by default.\n\n## 2.3.1 - 2021-09-22\n\n### Fixed\n\n- [ZipArchive] copy stream, the ziparchive is closed after getting the stream\n- [Core] PHP 8.1 compatibility updates\n- [LocalFilesystem] parse permissions during listing\n- [LocalFilesystem] clear realstatcache\n- [FTP] PHP 8.1 compatibility updates\n- [Core] Upgraded PHP-CS-Fixer\n\n## 2.3.0 - 2021-09-22\n\n### Added\n\n- [GoogleCloudStorage] Make it possible to set an empty prefix (#1358)\n- [GoogleCloudStorage] Added possibility not to set visibility (#1356)\n\n## 2.2.3 - 2021-08-18\n\n### Fixed\n\n- [Core] Corrected exception message for UnableToCopyFile.\n\n## 2.2.2 - 2021-08-18\n\n### Fixed\n\n- [Core] Ensure the sorted directory listing can be iterated over (#1342).\n\n## 2.2.1 - 2021-08-17\n\n### Fixed\n\n- [FTP] use original path when ensuring the parent directory exists during `move` operation.\n- [FTP] do not fail setting UTF-8 when the server is already on UTF-8.\n \n## 2.2.0 - 2021-07-20\n\n### Added\n\n* [Core] Added sortByPath on the directory listing to allows content listings to be sorted. \n\n## 2.1.1 - 2021-06-24\n\n### Fixed\n\n* [Core] Whitespace normalization now no longer strips funky whitespace but throws an exception.\n\n## 2.1.0 - 2021-05-25\n\n### Added\n\n* [Core] the DirectoryAttributes now have an `extraMetadata` like files do.\n\n### Fixed\n\n* [AwsS3V3] Allow the ACL config option to take precedence over the visibility key.\n\n## 2.0.8 - 2021-05-15\n\n### Fixed\n\n* [SFTP] Don't fail listing contents when a directory does not exist (#1301)\n\n## 2.0.7 - 2021-05-13\n\n### Fixed\n\n* [LocalFilesystem] convert windows style paths to unix style paths on listing\n\n## 2.0.6 - 2021-05-13\n\n### Fixed\n\n* [AsyncAwsS3] do not urlencode CopySource arguments (#1302)\n\n## 2.0.5 - 2021-04-11\n\n### Fixed\n\n* [AwsS3] ensure write errors are turned into exceptions. \n\n## 2.0.4 - 2021-02-13\n\n### Fixed\n\n* [InMemory] Corrected how the file size is determined.\n\n## 2.0.3 - 2021-02-09\n\n### Fixed\n\n* [AwsS3V3] Use the $config array during the copy operation.\n* [Ftp] Close FTP connections when the object is destructed.\n* [Core] Allow for an absolute root path of `/`.\n\n## 2.0.2 - 2020-12-28\n\n### Fixed\n\n* Corrected the ignored exports for Ftp\n\n## 2.0.1 - 2020-12-28\n\n### Fixed\n\n* Corrected the ignored exports for Phpseclib\n\n## 2.0.0 - 2020-11-24\n\n### Changed\n\n- string type added to all visibility input\n\n### Added\n\n- Google Cloud Storage adapter.\n\n## 2.0.0-beta.3 - 2020-08-23\n\n### Added\n\n- UnableToCheckFileExistence error introduced\n- Mount manager is re-introduced\n\n### Fixes\n\n- Allow FTP filenames to contain special characters.\n- Prevent resources of incorrect types to be passed.\n\n### Improved\n\n- [AWS] By default, make sure readStream resources are streamed over HTTP.\n\n### Added\n\n- DirectoryAttributes now have a `lastModified` accessor.\n\n## 2.0.0-beta.2 - 2020-08-08\n\n### Fixes\n\n- Allow listing of top-level directory for AWS S3\n- Ensure the adapters can use the correct beta release.\n\n## 2.0.0-beta.1 - 2020-08-04\n\n### Changes\n\n- Small code optimizations\n- Add global options array to AwsS3V3Adapter like in V1\n\n## 2.0.0-alpha.4 - 2020-07-26\n\n### Changes\n\n* Renamed AwsS3V3Filesystem to AwsS3V3Adapter (in line with other adapter names).\n* Renamed the PHPSecLibV2 package to PhpseclibV2, Renamed the FTP package to Ftp.\n* Public key and ss-agent authentication support for Sftp\n\n### Fixes\n\n* Allow creation of files with empty streams.\n\n## 2.0.0-alpha.3 - 2020-03-21\n\n### Fixes\n\n* Corrected the required version for the sub-split packages.\n\n## 2.0.0-alpha.2 - 2020-03-17\n\n### Changes\n\n* The `League\\Flysystem\\FilesystemAdapter::listContents` method returns an `iterable` instead of a `Generator`.\n* The `League\\Flysystem\\DirectoryListing` class accepts an `iterable` instead of a `Generator`.\n\n## 2.0.0-alpha.1 - 2020-03-09\n\n* Initial 2.0.0 alpha release\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\nconduct@flysystem.io.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "INFO.md",
    "content": "View the docs at: https://flysystem.thephpleague.com/docs/  \nChangelog at: https://github.com/thephpleague/flysystem/blob/3.x/CHANGELOG.md\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2013-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "bin/.gitignore",
    "content": "renamespace.php\n"
  },
  {
    "path": "bin/check-versions.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/**\n * This script check for composer dependency incompatibilities:.\n *\n *  - All required dependencies of the extracted packages MUST be\n *    present in the main composer.json's require(-dev) section.\n *  - Dependency constraints of extracted packages may not exclude\n *    the constraints of the main package and vice versa.\n *  - The provided target release argument must be satisfiable by\n *    all the extracted packages' core dependency constraint.\n */\n\nuse Composer\\Semver\\Comparator;\nuse Composer\\Semver\\Semver;\nuse Composer\\Semver\\VersionParser;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\Filesystem;\nuse League\\Flysystem\\Local\\LocalFilesystemAdapter;\nuse League\\Flysystem\\StorageAttributes;\n\ninclude_once __DIR__ . '/tools.php';\n\nfunction constraint_has_conflict(string $mainConstraint, string $packageConstraint): bool\n{\n    $parser = new VersionParser();\n    $mainConstraint = $parser->parseConstraints($mainConstraint);\n    $mainLowerBound = $mainConstraint->getLowerBound()->getVersion();\n    $mainUpperBound = $mainConstraint->getUpperBound()->getVersion();\n    $packageConstraint = $parser->parseConstraints($packageConstraint);\n    $packageLowerBound = $packageConstraint->getLowerBound()->getVersion();\n    $packageUpperBound = $packageConstraint->getUpperBound()->getVersion();\n\n    if (Comparator::compare($mainUpperBound, '<=', $packageLowerBound)) {\n        return true;\n    }\n\n    if (Comparator::compare($packageUpperBound, '<=', $mainLowerBound)) {\n        return true;\n    }\n\n    return false;\n}\n\nif ( ! isset($argv[1])) {\n    panic('No base version provided');\n}\n\nwrite_line(\"🔎 Inspecting composer dependency incompatibilities.\");\n\n$mainVersion = $argv[1];\n$filesystem = new Filesystem(new LocalFilesystemAdapter(__DIR__ . '/../'));\n\n$mainComposer = $filesystem->read('composer.json');\n/** @var string[] $otherComposers */\n$otherComposers = $filesystem->listContents('src', true)\n    ->filter(function (StorageAttributes $item) {\n        return $item->isFile();\n    })\n    ->filter(function (FileAttributes $item) {\n        return substr($item->path(), -5) === '.json';\n    })\n    ->map(function (FileAttributes $item) {\n        return $item->path();\n    })\n    ->toArray();\n\n$mainInformation = json_decode($mainComposer, true);\n\nforeach ($otherComposers as $composerFile) {\n    $information = json_decode($filesystem->read($composerFile), true);\n\n    foreach ($information['require'] as $dependency => $constraint) {\n        if (str_starts_with($dependency, 'ext-') || $dependency === 'phpseclib/phpseclib') {\n            continue;\n        }\n\n        if ($dependency === 'league/flysystem') {\n            if ( ! Semver::satisfies($mainVersion, $constraint)) {\n                panic(\"Composer file {$composerFile} does not allow league/flysystem:{$mainVersion}\");\n            } else {\n                write_line(\"Composer file {$composerFile} allows league/flysystem:{$mainVersion} with {$constraint}\");\n            }\n\n            continue;\n        }\n\n        $mainDependencyConstraint = $mainInformation['require'][$dependency]\n            ?? $mainInformation['require-dev'][$dependency]\n            ?? null;\n\n        if ( ! is_string($mainDependencyConstraint)) {\n            panic(\n                \"The main composer file does not depend on an adapter dependency.\\n\" .\n                \"Depedency {$dependency} from {$composerFile} is missing.\"\n            );\n        }\n\n        if (constraint_has_conflict($mainDependencyConstraint, $constraint)) {\n            panic(\n                \"Package constraints are conflicting:\\n\\n\" .\n                \"Package composer file: {$composerFile}\\n\" .\n                \"Dependency name: {$dependency}\\n\" .\n                \"Main constraint: {$mainDependencyConstraint}\\n\" .\n                \"Package constraint: {$constraint}\"\n            );\n        }\n    }\n}\n\nwrite_line(\"✅ Composer dependencies are looking fine.\");\n"
  },
  {
    "path": "bin/close-subsplit-prs.yml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "bin/set-flysystem-version.php",
    "content": "<?php\n\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\Filesystem;\nuse League\\Flysystem\\Local\\LocalFilesystemAdapter;\nuse League\\Flysystem\\StorageAttributes;\n\ninclude_once __DIR__ . '/tools.php';\n\nif ( ! isset($argv[1])) {\n    panic('No base version provided');\n}\n\n$mainVersion = $argv[1];\n\nwrite_line(\"☝️ Setting all flysystem constraints to {$mainVersion}.\");\n\n$filesystem = new Filesystem(new LocalFilesystemAdapter(__DIR__ . '/../'));\n\n/** @var string[] $otherComposers */\n$composerFiles = $filesystem->listContents('src', true)\n    ->filter(function (StorageAttributes $item) {\n        return $item->isFile();\n    })\n    ->filter(function (FileAttributes $item) {\n        return substr($item->path(), -5) === '.json';\n    })\n    ->map(function (FileAttributes $item) {\n        return $item->path();\n    })\n    ->toArray();\n\nforeach ($composerFiles as $composerFile) {\n    $contents = $filesystem->read($composerFile);\n    $mainVersionRegex = preg_quote($mainVersion, '~');\n    $updated = preg_replace('~(\"league/flysystem\": \"\\\\^[a-zA-Z0-9\\\\.-]+\")~ms', '\"league/flysystem\": \"^' . $mainVersion . '\"', $contents);\n    $filesystem->write($composerFile, $updated);\n}\n"
  },
  {
    "path": "bin/tools.php",
    "content": "<?php\n\ninclude_once __DIR__ . '/../vendor/autoload.php';\n\nfunction write_line(string $line)\n{\n    fwrite(STDOUT, \"{$line}\\n\");\n}\n\nfunction panic(string $reason)\n{\n    write_line('🚨 ' . $reason);\n    exit(1);\n}\n"
  },
  {
    "path": "bin/update-subsplit-closers.php",
    "content": "<?php\n\nuse League\\Flysystem\\Filesystem;\nuse League\\Flysystem\\Local\\LocalFilesystemAdapter;\n\ninclude __DIR__ . '/../vendor/autoload.php';\n\n$filesystem = new Filesystem(new LocalFilesystemAdapter(realpath(__DIR__ . '/../')));\n$subsplits = json_decode($filesystem->read('config.subsplit-publish.json'), true);\n$workflowContents = $filesystem->read('bin/close-subsplit-prs.yml');\n\nforeach ($subsplits['sub-splits'] as ['directory' => $subsplit]) {\n    $workflowPath = $subsplit . '/.github/workflows/close-subsplit-prs.yaml';\n    $filesystem->write($workflowPath, $workflowContents);\n}\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"league/flysystem\",\n    \"description\": \"File storage abstraction for PHP\",\n    \"keywords\": [\n        \"filesystem\", \"filesystems\", \"files\", \"storage\", \"aws\",\n        \"s3\", \"ftp\", \"sftp\", \"webdav\", \"file\", \"cloud\"\n    ],\n    \"scripts\": {\n        \"phpstan\": \"vendor/bin/phpstan analyse -l 6 src\"\n    },\n    \"type\": \"library\",\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"autoload\": {\n        \"psr-4\": {\n            \"League\\\\Flysystem\\\\\": \"src\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"league/flysystem-local\":  \"^3.0.0\",\n        \"league/mime-type-detection\": \"^1.0.0\"\n    },\n    \"require-dev\": {\n        \"ext-zip\": \"*\",\n        \"ext-fileinfo\": \"*\",\n        \"ext-ftp\": \"*\",\n        \"ext-mongodb\": \"^1.3|^2\",\n        \"microsoft/azure-storage-blob\": \"^1.1\",\n        \"phpunit/phpunit\": \"^9.5.11|^10.0\",\n        \"phpstan/phpstan\": \"^1.10\",\n        \"phpseclib/phpseclib\": \"^3.0.36\",\n        \"aws/aws-sdk-php\": \"^3.295.10\",\n        \"composer/semver\": \"^3.0\",\n        \"friendsofphp/php-cs-fixer\": \"^3.5\",\n        \"google/cloud-storage\": \"^1.23\",\n        \"async-aws/s3\": \"^1.5 || ^2.0\",\n        \"async-aws/simple-s3\": \"^1.1 || ^2.0\",\n        \"mongodb/mongodb\": \"^1.2|^2\",\n        \"sabre/dav\": \"^4.6.0\",\n        \"guzzlehttp/psr7\": \"^2.6\"\n    },\n    \"conflict\": {\n        \"async-aws/core\": \"<1.19.0\",\n        \"async-aws/s3\": \"<1.14.0\",\n        \"symfony/http-client\": \"<5.2\",\n        \"guzzlehttp/ringphp\": \"<1.1.1\",\n        \"guzzlehttp/guzzle\": \"<7.0\",\n        \"aws/aws-sdk-php\": \"3.209.31 || 3.210.0\",\n        \"phpseclib/phpseclib\": \"3.0.15\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ],\n    \"repositories\": [\n        {\n            \"type\": \"package\",\n            \"package\": {\n                \"name\": \"league/flysystem-local\",\n                \"version\": \"3.0.0\",\n                \"dist\": {\n                    \"type\": \"path\",\n                    \"url\": \"src/Local\"\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "config.subsplit-publish.json",
    "content": "{\n    \"sub-splits\": [\n        {\n            \"name\": \"ftp\",\n            \"directory\": \"src/Ftp\",\n            \"target\": \"git@github.com:thephpleague/flysystem-ftp.git\"\n        },\n        {\n            \"name\": \"sftp\",\n            \"directory\": \"src/PhpseclibV2\",\n            \"target\": \"git@github.com:thephpleague/flysystem-sftp.git\"\n        },\n        {\n            \"name\": \"sftp-v3\",\n            \"directory\": \"src/PhpseclibV3\",\n            \"target\": \"git@github.com:thephpleague/flysystem-sftp-v3.git\"\n        },\n        {\n            \"name\": \"memory\",\n            \"directory\": \"src/InMemory\",\n            \"target\": \"git@github.com:thephpleague/flysystem-memory.git\"\n        },\n        {\n            \"name\": \"local\",\n            \"directory\": \"src/Local\",\n            \"target\": \"git@github.com:thephpleague/flysystem-local.git\"\n        },\n        {\n            \"name\": \"ziparchive\",\n            \"directory\": \"src/ZipArchive\",\n            \"target\": \"git@github.com:thephpleague/flysystem-ziparchive.git\"\n        },\n        {\n            \"name\": \"aws-s3-v3\",\n            \"directory\": \"src/AwsS3V3\",\n            \"target\": \"git@github.com:thephpleague/flysystem-aws-s3-v3.git\"\n        },\n        {\n            \"name\": \"async-aws-s3\",\n            \"directory\": \"src/AsyncAwsS3\",\n            \"target\": \"git@github.com:thephpleague/flysystem-async-aws-s3.git\"\n        },\n        {\n            \"name\": \"azure-blob-storage\",\n            \"directory\": \"src/AzureBlobStorage\",\n            \"target\": \"git@github.com:thephpleague/flysystem-azure-blob-storage.git\"\n        },\n        {\n            \"name\": \"google-cloud-storage\",\n            \"directory\": \"src/GoogleCloudStorage\",\n            \"target\": \"git@github.com:thephpleague/flysystem-google-cloud-storage.git\"\n        },\n        {\n            \"name\": \"readonly\",\n            \"directory\": \"src/ReadOnly\",\n            \"target\": \"git@github.com:thephpleague/flysystem-read-only.git\"\n        },\n        {\n            \"name\": \"pathprefixing\",\n            \"directory\": \"src/PathPrefixing\",\n            \"target\": \"git@github.com:thephpleague/flysystem-path-prefixing.git\"\n        },\n        {\n            \"name\": \"webdav\",\n            \"directory\": \"src/WebDAV\",\n            \"target\": \"git@github.com:thephpleague/flysystem-webdav.git\"\n        },\n        {\n            \"name\": \"adapter-test-utilities\",\n            \"directory\": \"src/AdapterTestUtilities\",\n            \"target\": \"git@github.com:thephpleague/flysystem-adapter-test-utilities.git\"\n        },\n        {\n            \"name\": \"gridfs\",\n            \"directory\": \"src/GridFS\",\n            \"target\": \"git@github.com:thephpleague/flysystem-gridfs.git\"\n        }\n    ]\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "---\nversion: \"3\"\nservices:\n  mongodb:\n    image: mongo:7\n    ports:\n      - \"27017:27017\"\n  sabredav:\n    image: php:8.1-alpine3.15\n    restart: always\n    volumes:\n      - ./:/var/www/html/\n    ports:\n      - \"4040:4040\"\n    command: php -S 0.0.0.0:4040 /var/www/html/src/WebDAV/resources/server.php\n  webdav:\n    image: bytemark/webdav\n    restart: always\n    ports:\n      - \"4080:80\"\n    environment:\n      AUTH_TYPE: Digest\n      USERNAME: alice\n      PASSWORD: secret1234\n      ANONYMOUS_METHODS: 'GET,OPTIONS'\n  sftp:\n    container_name: sftp\n    restart: always\n    image: atmoz/sftp\n    volumes:\n      - ./test_files/sftp/users.conf:/etc/sftp/users.conf\n      - ./test_files/sftp/ssh_host_ed25519_key:/etc/ssh/ssh_host_ed25519_key\n      - ./test_files/sftp/ssh_host_rsa_key:/etc/ssh/ssh_host_rsa_key\n      - ./test_files/sftp/id_rsa.pub:/home/bar/.ssh/keys/id_rsa.pub\n    ports:\n      - \"2222:22\"\n  sftp_eddsa_only:\n    container_name: sftp_eddsa_only\n    restart: always\n    image: atmoz/sftp\n    volumes:\n      - ./test_files/sftp/users.conf:/etc/sftp/users.conf\n      - ./test_files/sftp/sshd_custom_configs.sh:/etc/sftp.d/sshd_custom_configs.sh\n      - ./test_files/sftp/ssh_host_ed25519_key:/etc/ssh/ssh_host_ed25519_key\n    ports:\n      - \"2223:22\"\n  ftp:\n    container_name: ftp\n    restart: always\n    image: delfer/alpine-ftp-server\n    environment:\n      USERS: 'foo|pass|/home/foo/upload'\n      ADDRESS: 'localhost'\n    ports:\n      - \"2121:21\"\n      - \"21000-21010:21000-21010\"\n  ftpd:\n    container_name: ftpd\n    restart: always\n    environment:\n      PUBLICHOST: localhost\n      FTP_USER_NAME: foo\n      FTP_USER_PASS: pass\n      FTP_USER_HOME: /home/foo\n    image: stilliard/pure-ftpd\n    ports:\n      - \"2122:21\"\n      - \"30000-30009:30000-30009\"\n    command: \"/run.sh -l puredb:/etc/pure-ftpd/pureftpd.pdb -E -j -P localhost\"\n  toxiproxy:\n    container_name: toxiproxy\n    restart: unless-stopped\n    image: ghcr.io/shopify/toxiproxy\n    command: \"-host 0.0.0.0 -config /opt/toxiproxy/config.json\"\n    volumes:\n      - ./test_files/toxiproxy/toxiproxy.json:/opt/toxiproxy/config.json:ro\n    ports:\n      - \"8474:8474\" # HTTP API\n      - \"8222:8222\" # SFTP\n      - \"8121:8121\" # FTP\n      - \"8122:8122\" # FTPD\n"
  },
  {
    "path": "mocked-functions.php",
    "content": "<?php\n\nnamespace League\\Flysystem\\Local {\n    function rmdir(...$arguments)\n    {\n        if ( ! is_mocked('rmdir')) {\n            return \\rmdir(...$arguments);\n        }\n\n        return return_mocked_value('rmdir');\n    }\n\n    function unlink(...$arguments)\n    {\n        if ( ! is_mocked('unlink')) {\n            return \\unlink(...$arguments);\n        }\n\n        return return_mocked_value('unlink');\n    }\n\n    function filemtime(...$arguments)\n    {\n        if ( ! is_mocked('filemtime')) {\n            return \\filemtime(...$arguments);\n        }\n\n        return return_mocked_value('filemtime');\n    }\n\n    function filesize(...$arguments)\n    {\n        if ( ! is_mocked('filesize')) {\n            return \\filesize(...$arguments);\n        }\n\n        return return_mocked_value('filesize');\n    }\n}\n\nnamespace League\\Flysystem\\InMemory {\n    function time()\n    {\n        if ( ! is_mocked('time')) {\n            return \\time();\n        }\n\n        return return_mocked_value('time');\n    }\n}\n\nnamespace League\\Flysystem\\Ftp {\n    function ftp_raw(...$arguments)\n    {\n        if ( ! is_mocked('ftp_raw')) {\n            return \\ftp_raw(...$arguments);\n        }\n\n        return return_mocked_value('ftp_raw');\n    }\n\n    function ftp_set_option(...$arguments)\n    {\n        if ( ! is_mocked('ftp_set_option')) {\n            return \\ftp_set_option(...$arguments);\n        }\n\n        return return_mocked_value('ftp_set_option');\n    }\n\n    function ftp_pasv(...$arguments)\n    {\n        if ( ! is_mocked('ftp_pasv')) {\n            return \\ftp_pasv(...$arguments);\n        }\n\n        return return_mocked_value('ftp_pasv');\n    }\n\n    function ftp_pwd(...$arguments)\n    {\n        if ( ! is_mocked('ftp_pwd')) {\n            return \\ftp_pwd(...$arguments);\n        }\n\n        return return_mocked_value('ftp_pwd');\n    }\n\n    function ftp_fput(...$arguments)\n    {\n        if ( ! is_mocked('ftp_fput')) {\n            return \\ftp_fput(...$arguments);\n        }\n\n        return return_mocked_value('ftp_fput');\n    }\n\n    function ftp_chmod(...$arguments)\n    {\n        if ( ! is_mocked('ftp_chmod')) {\n            return \\ftp_chmod(...$arguments);\n        }\n\n        return return_mocked_value('ftp_chmod');\n    }\n\n    function ftp_mkdir(...$arguments)\n    {\n        if ( ! is_mocked('ftp_mkdir')) {\n            return \\ftp_mkdir(...$arguments);\n        }\n\n        return return_mocked_value('ftp_mkdir');\n    }\n\n    function ftp_delete(...$arguments)\n    {\n        if ( ! is_mocked('ftp_delete')) {\n            return \\ftp_delete(...$arguments);\n        }\n\n        return return_mocked_value('ftp_delete');\n    }\n\n    function ftp_rmdir(...$arguments)\n    {\n        if ( ! is_mocked('ftp_rmdir')) {\n            return \\ftp_rmdir(...$arguments);\n        }\n\n        return return_mocked_value('ftp_rmdir');\n    }\n\n    function ftp_fget(...$arguments)\n    {\n        if ( ! is_mocked('ftp_fget')) {\n            return \\ftp_fget(...$arguments);\n        }\n\n        return return_mocked_value('ftp_fget');\n    }\n\n    function ftp_rawlist(...$arguments)\n    {\n        if ( ! is_mocked('ftp_rawlist')) {\n            return \\ftp_rawlist(...$arguments);\n        }\n\n        return return_mocked_value('ftp_rawlist');\n    }\n}\n\nnamespace League\\Flysystem\\ZipArchive {\n    function stream_get_contents(...$arguments)\n    {\n        if ( ! is_mocked('stream_get_contents')) {\n            return \\stream_get_contents(...$arguments);\n        }\n\n        return return_mocked_value('stream_get_contents');\n    }\n}\n"
  },
  {
    "path": "phpstan-baseline.neon",
    "content": "parameters:\n\tignoreErrors:\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\$connection of method League\\\\\\\\Flysystem\\\\\\\\Ftp\\\\\\\\FtpAdapter\\\\:\\\\:resolveConnectionRoot\\\\(\\\\) has invalid type FTP\\\\\\\\Connection\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Unsafe access to private property League\\\\\\\\Flysystem\\\\\\\\GoogleCloudStorage\\\\\\\\GoogleCloudStorageAdapter\\\\:\\\\:\\\\$algoToInfoMap through static\\\\:\\\\:\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/GoogleCloudStorage/GoogleCloudStorageAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Unsafe access to private property League\\\\\\\\Flysystem\\\\\\\\GoogleCloudStorage\\\\\\\\GoogleCloudStorageAdapterTest\\\\:\\\\:\\\\$adapterPrefix through static\\\\:\\\\:\\\\.$#\"\n\t\t\tcount: 3\n\t\t\tpath: src/GoogleCloudStorage/GoogleCloudStorageAdapterTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Unsafe access to private property League\\\\\\\\Flysystem\\\\\\\\GoogleCloudStorage\\\\\\\\GoogleCloudStorageAdapterTest\\\\:\\\\:\\\\$bucket through static\\\\:\\\\:\\\\.$#\"\n\t\t\tcount: 5\n\t\t\tpath: src/GoogleCloudStorage/GoogleCloudStorageAdapterTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Unsafe access to private property League\\\\\\\\Flysystem\\\\\\\\GoogleCloudStorage\\\\\\\\GoogleCloudStorageAdapterTest\\\\:\\\\:\\\\$prefixer through static\\\\:\\\\:\\\\.$#\"\n\t\t\tcount: 3\n\t\t\tpath: src/GoogleCloudStorage/GoogleCloudStorageAdapterTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Offset 2 does not exist on array\\\\{League\\\\\\\\Flysystem\\\\\\\\FilesystemOperator, string\\\\}\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/MountManager.php\n\n\t\t-\n\t\t\tmessage: \"#^Unsafe access to private property League\\\\\\\\Flysystem\\\\\\\\ZipArchive\\\\\\\\ZipArchiveAdapterTestCase\\\\:\\\\:\\\\$archiveProvider through static\\\\:\\\\:\\\\.$#\"\n\t\t\tcount: 10\n\t\t\tpath: src/ZipArchive/ZipArchiveAdapterTestCase.php\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_chdir expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 2\n\t\t\tpath: src/Ftp/FtpAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_chmod expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 2\n\t\t\tpath: src/Ftp/FtpAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_delete expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_fget expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_fput expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_mdtm expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_mkdir expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_raw expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_rawlist expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_rename expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_rmdir expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_size expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 2\n\t\t\tpath: src/Ftp/FtpAdapter.php\n\n\t\t-\n\t\t\tmessage: \"#^Method League\\\\\\\\Flysystem\\\\\\\\Ftp\\\\\\\\FtpConnectionProvider\\\\:\\\\:createConnectionResource\\\\(\\\\) should return resource but returns FTP\\\\\\\\Connection\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpConnectionProvider.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_close expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpConnectionProvider.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_login expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpConnectionProvider.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_pasv expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpConnectionProvider.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_raw expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpConnectionProvider.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_set_option expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpConnectionProvider.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_close expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/FtpConnectionProviderTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_raw expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/NoopCommandConnectivityChecker.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_close expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/NoopCommandConnectivityCheckerTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_rawlist expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/RawListFtpConnectivityChecker.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$ftp of function ftp_close expects FTP\\\\\\\\Connection, resource given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Ftp/RawListFtpConnectivityCheckerTest.php\n"
  },
  {
    "path": "phpstan.neon",
    "content": "includes:\n    - phpstan-baseline.neon\nparameters:\n    level: 6\n    paths:\n        - src\n    checkMissingIterableValueType: false\n    reportUnmatchedIgnoredErrors: false\n    checkGenericClassInNonGenericObjectType: false\n    scanFiles:\n        - src/AdapterTestUtilities/test-functions.php\n    excludePaths:\n        - src/AdapterTestUtilities/\n        - src/AsyncAwsS3\n        - src/AwsS3V3\n        - src/FTP\n        - src/InMemory\n        - src/PhpseclibV2\n        - src/PhpseclibV3\n    ignoreErrors:\n        - '#invalid typehint type FTP\\\\Connection#'\n        - '#FTP\\\\Connection not found#'\n        - '#unknown class FTP\\\\Connection#'\n        - '#Call to function iterator_to_array\\(\\) on a separate line has no effect\\.#'\n        - '#Comparison operation \"<\" between 0|1 and 4 is always true.#'\n        - '#Method League\\\\Flysystem\\\\AwsS3V3\\\\S3ClientStub.*#'\n        - '#Constant NET_SFTP_TYPE_DIRECTORY not found\\.#'\n        - '#\\$local_file of method phpseclib\\\\Net\\\\SFTP::get\\(\\) expects string, resource given#'\n"
  },
  {
    "path": "phpunit.php",
    "content": "<?php\n\ninclude __DIR__ . '/vendor/autoload.php';\ninclude __DIR__ . '/src/AdapterTestUtilities/test-functions.php';\ninclude __DIR__ . '/mocked-functions.php';\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit colors=\"true\" bootstrap=\"phpunit.php\">\n    <testsuites>\n        <testsuite name=\"Flysystem\">\n            <directory suffix=\"Test.php\">src/</directory>\n        </testsuite>\n    </testsuites>\n    <php>\n        <env name=\"FLYSYSTEM_TEST_SFTP\" value=\"yes\" />\n    </php>\n    <groups>\n        <exclude>\n            <group>legacy</group>\n        </exclude>\n    </groups>\n    <coverage>\n        <include>\n            <directory suffix=\".php\">src</directory>\n        </include>\n    </coverage>\n</phpunit>\n"
  },
  {
    "path": "readme.md",
    "content": "# League\\Flysystem\n\n[![Author](https://img.shields.io/badge/author-@frankdejonge-blue.svg)](https://twitter.com/frankdejonge)\n[![Source Code](https://img.shields.io/badge/source-thephpleague/flysystem-blue.svg)](https://github.com/thephpleague/flysystem)\n[![Latest Version](https://img.shields.io/github/tag/thephpleague/flysystem.svg)](https://github.com/thephpleague/flysystem/releases)\n[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/thephpleague/flysystem/blob/master/LICENSE)\n[![Quality Assurance](https://github.com/thephpleague/flysystem/workflows/Quality%20Assurance/badge.svg?branch=2.x)](https://github.com/thephpleague/flysystem/actions?query=workflow%3A%22Quality+Assurance%22)\n[![Total Downloads](https://img.shields.io/packagist/dt/league/flysystem.svg)](https://packagist.org/packages/league/flysystem)\n![php 7.2+](https://img.shields.io/badge/php-min%208.0.2-red.svg)\n\n## About Flysystem\n\nFlysystem is a file storage library for PHP. It provides one interface to\ninteract with many types of filesystems. When you use Flysystem, you're\nnot only protected from vendor lock-in, you'll also have a consistent experience\nfor which ever storage is right for you. \n\n## Getting Started\n\n* **[New in V3](https://flysystem.thephpleague.com/docs/what-is-new/)**: What is new in Flysystem V2/V3?\n* **[Architecture](https://flysystem.thephpleague.com/docs/architecture/)**: Flysystem's internal architecture\n* **[Flysystem API](https://flysystem.thephpleague.com/docs/usage/filesystem-api/)**: How to interact with your Flysystem instance\n* **[Upgrade from 1x](https://flysystem.thephpleague.com/docs/upgrade-from-1.x/)**: How to upgrade from 1.x/2.x\n\n### Officially supported adapters\n\n* **[Local](https://flysystem.thephpleague.com/docs/adapter/local/)**\n* **[FTP](https://flysystem.thephpleague.com/docs/adapter/ftp/)**\n* **[SFTP](https://flysystem.thephpleague.com/docs/adapter/sftp-v3/)**\n* **[Memory](https://flysystem.thephpleague.com/docs/adapter/in-memory/)**\n* **[AWS S3](https://flysystem.thephpleague.com/docs/adapter/aws-s3-v3/)**\n* **[AsyncAws S3](https://flysystem.thephpleague.com/docs/adapter/async-aws-s3/)**\n* **[Google Cloud Storage](https://flysystem.thephpleague.com/docs/adapter/google-cloud-storage/)**\n* **[MongoDB GridFS](https://flysystem.thephpleague.com/docs/adapter/gridfs/)**\n* **[WebDAV](https://flysystem.thephpleague.com/docs/adapter/webdav/)**\n* **[ZipArchive](https://flysystem.thephpleague.com/docs/adapter/zip-archive/)**\n\n### Third party Adapters\n\n* **[Azure Blob Storage](https://github.com/Azure-OSS/azure-storage-php-adapter-flysystem)**\n* **[Gitlab](https://github.com/RoyVoetman/flysystem-gitlab-storage)**\n* **[Google Drive (using regular paths)](https://github.com/masbug/flysystem-google-drive-ext)**\n* **[bunny.net / BunnyCDN](https://github.com/PlatformCommunity/flysystem-bunnycdn/tree/v3)**\n* **[Sharepoint 365 / One Drive (Using MS Graph)](https://github.com/shitware-ltd/flysystem-msgraph)**\n* **[OneDrive](https://github.com/doerffler/flysystem-onedrive)**\n* **[Dropbox](https://github.com/spatie/flysystem-dropbox)**\n* **[ReplicateAdapter](https://github.com/ajgarlag/flysystem-replicate)**\n* **[Uploadcare](https://github.com/vormkracht10/flysystem-uploadcare)**\n* **[Useful adapters (FallbackAdapter, LogAdapter, ReadWriteAdapter, RetryAdapter)](https://github.com/ElGigi/FlysystemUsefulAdapters)**\n* **[Metadata Cache](https://github.com/jgivoni/flysystem-cache-adapter)**\n* **[Migration adapter (lazy)](https://github.com/antonsacred/flysystem-lazy-migration-adapter)**\n\nYou can always [create an adapter](https://flysystem.thephpleague.com/docs/advanced/creating-an-adapter/) yourself.\n\n## Security\n\nIf you discover any security related issues, please email info@frankdejonge.nl instead of using the issue tracker.\n\n## Enjoy\n\nOh, and if you've come down this far, you might as well follow me on [twitter](https://twitter.com/frankdejonge).\n"
  },
  {
    "path": "src/AdapterTestUtilities/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\nREADME.md export-ignore\n"
  },
  {
    "path": "src/AdapterTestUtilities/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/AdapterTestUtilities/ExceptionThrowingFilesystemAdapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AdapterTestUtilities;\n\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\FilesystemOperationFailed;\n\nclass ExceptionThrowingFilesystemAdapter implements FilesystemAdapter\n{\n    /**\n     * @var FilesystemAdapter\n     */\n    private $adapter;\n\n    /**\n     * @var array<string, FilesystemOperationFailed>\n     */\n    private $stagedExceptions = [];\n\n    public function __construct(FilesystemAdapter $adapter)\n    {\n        $this->adapter = $adapter;\n    }\n\n    public function stageException(string $method, string $path, FilesystemOperationFailed $exception): void\n    {\n        $this->stagedExceptions[join('@', [$method, $path])] = $exception;\n    }\n\n    private function throwStagedException(string $method, $path): void\n    {\n        $method = preg_replace('~.+::~', '', $method);\n        $key = join('@', [$method, $path]);\n\n        if ( ! array_key_exists($key, $this->stagedExceptions)) {\n            return;\n        }\n\n        $exception = $this->stagedExceptions[$key];\n        unset($this->stagedExceptions[$key]);\n        throw $exception;\n    }\n\n    public function fileExists(string $path): bool\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        return $this->adapter->fileExists($path);\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        $this->adapter->write($path, $contents, $config);\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        $this->adapter->writeStream($path, $contents, $config);\n    }\n\n    public function read(string $path): string\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        return $this->adapter->read($path);\n    }\n\n    public function readStream(string $path)\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        return $this->adapter->readStream($path);\n    }\n\n    public function delete(string $path): void\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        $this->adapter->delete($path);\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        $this->adapter->deleteDirectory($path);\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        $this->adapter->createDirectory($path, $config);\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        $this->adapter->setVisibility($path, $visibility);\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        return $this->adapter->visibility($path);\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        return $this->adapter->mimeType($path);\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        return $this->adapter->lastModified($path);\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        return $this->adapter->fileSize($path);\n    }\n\n    public function listContents(string $path, bool $deep): iterable\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        return $this->adapter->listContents($path, $deep);\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        $this->throwStagedException(__METHOD__, $source);\n\n        $this->adapter->move($source, $destination, $config);\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        $this->throwStagedException(__METHOD__, $source);\n\n        $this->adapter->copy($source, $destination, $config);\n    }\n\n    public function directoryExists(string $path): bool\n    {\n        $this->throwStagedException(__METHOD__, $path);\n\n        return $this->adapter->directoryExists($path);\n    }\n}\n"
  },
  {
    "path": "src/AdapterTestUtilities/FilesystemAdapterTestCase.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AdapterTestUtilities;\n\nuse const PHP_EOL;\nuse DateInterval;\nuse DateTimeImmutable;\nuse Generator;\nuse League\\Flysystem\\ChecksumProvider;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\StorageAttributes;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToProvideChecksum;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UrlGeneration\\PublicUrlGenerator;\nuse League\\Flysystem\\UrlGeneration\\TemporaryUrlGenerator;\nuse League\\Flysystem\\Visibility;\nuse PHPUnit\\Framework\\TestCase;\nuse Throwable;\nuse function file_get_contents;\nuse function is_resource;\nuse function iterator_to_array;\n\n/**\n * @codeCoverageIgnore\n */\nabstract class FilesystemAdapterTestCase extends TestCase\n{\n    use RetryOnTestException;\n\n    /**\n     * @var FilesystemAdapter\n     */\n    protected static $adapter;\n\n    /**\n     * @var bool\n     */\n    protected $isUsingCustomAdapter = false;\n\n    public static function clearFilesystemAdapterCache(): void\n    {\n        static::$adapter = null;\n    }\n\n    abstract protected static function createFilesystemAdapter(): FilesystemAdapter;\n\n    public function adapter(): FilesystemAdapter\n    {\n        if ( ! static::$adapter instanceof FilesystemAdapter) {\n            static::$adapter = static::createFilesystemAdapter();\n        }\n\n        return static::$adapter;\n    }\n\n    public static function tearDownAfterClass(): void\n    {\n        self::clearFilesystemAdapterCache();\n    }\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this->adapter();\n    }\n\n    protected function useAdapter(FilesystemAdapter $adapter): FilesystemAdapter\n    {\n        static::$adapter = $adapter;\n        $this->isUsingCustomAdapter = true;\n\n        return $adapter;\n    }\n\n    /**\n     * @after\n     */\n    public function cleanupAdapter(): void\n    {\n        $this->clearCustomAdapter();\n        $this->clearStorage();\n    }\n\n    public function clearStorage(): void\n    {\n        reset_function_mocks();\n\n        try {\n            $adapter = $this->adapter();\n        } catch (Throwable $exception) {\n            /*\n             * Setting up the filesystem adapter failed. This is OK at this stage.\n             * The exception will have been shown to the user when trying to run\n             * a test. We expect an exception to be thrown when tests are marked as\n             * skipped when a filesystem adapter cannot be constructed.\n             */\n            return;\n        }\n\n        $this->runSetup(function () use ($adapter) {\n            /** @var StorageAttributes $item */\n            foreach ($adapter->listContents('', false) as $item) {\n                if ($item->isDir()) {\n                    $adapter->deleteDirectory($item->path());\n                } else {\n                    $adapter->delete($item->path());\n                }\n            }\n        });\n    }\n\n    public function clearCustomAdapter(): void\n    {\n        if ($this->isUsingCustomAdapter) {\n            $this->isUsingCustomAdapter = false;\n            self::clearFilesystemAdapterCache();\n        }\n    }\n\n    /**\n     * @test\n     */\n    public function writing_and_reading_with_string(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n\n            $adapter->write('path.txt', 'contents', new Config());\n            $fileExists = $adapter->fileExists('path.txt');\n            $contents = $adapter->read('path.txt');\n\n            $this->assertTrue($fileExists);\n            $this->assertEquals('contents', $contents);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function writing_a_file_with_a_stream(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $writeStream = stream_with_contents('contents');\n\n            $adapter->writeStream('path.txt', $writeStream, new Config([\n                Config::OPTION_VISIBILITY => Visibility::PUBLIC,\n            ]));\n\n            if (is_resource($writeStream)) {\n                fclose($writeStream);\n            }\n\n            $fileExists = $adapter->fileExists('path.txt');\n\n            $this->assertTrue($fileExists);\n        });\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider filenameProvider\n     */\n    public function writing_and_reading_files_with_special_path(string $path): void\n    {\n        $this->runScenario(function () use ($path) {\n            $adapter = $this->adapter();\n\n            $adapter->write($path, 'contents', new Config());\n            $contents = $adapter->read($path);\n\n            $this->assertEquals('contents', $contents);\n        });\n    }\n\n    public static function filenameProvider(): Generator\n    {\n        yield \"a path with square brackets in filename 1\" => [\"some/file[name].txt\"];\n        yield \"a path with square brackets in filename 2\" => [\"some/file[0].txt\"];\n        yield \"a path with square brackets in filename 3\" => [\"some/file[10].txt\"];\n        yield \"a path with square brackets in dirname 1\" => [\"some[name]/file.txt\"];\n        yield \"a path with square brackets in dirname 2\" => [\"some[0]/file.txt\"];\n        yield \"a path with square brackets in dirname 3\" => [\"some[10]/file.txt\"];\n        yield \"a path with curly brackets in filename 1\" => [\"some/file{name}.txt\"];\n        yield \"a path with curly brackets in filename 2\" => [\"some/file{0}.txt\"];\n        yield \"a path with curly brackets in filename 3\" => [\"some/file{10}.txt\"];\n        yield \"a path with curly brackets in dirname 1\" => [\"some{name}/filename.txt\"];\n        yield \"a path with curly brackets in dirname 2\" => [\"some{0}/filename.txt\"];\n        yield \"a path with curly brackets in dirname 3\" => [\"some{10}/filename.txt\"];\n        yield \"a path with space in dirname\" => [\"some dir/filename.txt\"];\n        yield \"a path with space in filename\" => [\"somedir/file name.txt\"];\n    }\n\n    /**\n     * @test\n     */\n    public function writing_a_file_with_an_empty_stream(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $writeStream = stream_with_contents('');\n\n            $adapter->writeStream('path.txt', $writeStream, new Config());\n\n            if (is_resource($writeStream)) {\n                fclose($writeStream);\n            }\n\n            $fileExists = $adapter->fileExists('path.txt');\n\n            $this->assertTrue($fileExists);\n\n            $contents = $adapter->read('path.txt');\n            $this->assertEquals('', $contents);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function listing_a_directory_named_0(): void\n    {\n        $this->givenWeHaveAnExistingFile('0/path.txt');\n        $this->givenWeHaveAnExistingFile('1/path.txt');\n\n        $this->runScenario(function () {\n            $listing = iterator_to_array($this->adapter()->listContents('0', false));\n\n            $this->assertCount(1, $listing);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function reading_a_file(): void\n    {\n        $this->givenWeHaveAnExistingFile('path.txt', 'contents');\n\n        $this->runScenario(function () {\n            $contents = $this->adapter()->read('path.txt');\n\n            $this->assertEquals('contents', $contents);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function reading_a_file_with_a_stream(): void\n    {\n        $this->givenWeHaveAnExistingFile('path.txt', 'contents');\n\n        $this->runScenario(function () {\n            $readStream = $this->adapter()->readStream('path.txt');\n            $contents = stream_get_contents($readStream);\n\n            $this->assertIsResource($readStream);\n            $this->assertEquals('contents', $contents);\n            fclose($readStream);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function overwriting_a_file(): void\n    {\n        $this->runScenario(function () {\n            $this->givenWeHaveAnExistingFile('path.txt', 'contents', ['visibility' => Visibility::PUBLIC]);\n            $adapter = $this->adapter();\n\n            $adapter->write('path.txt', 'new contents', new Config(['visibility' => Visibility::PRIVATE]));\n\n            $contents = $adapter->read('path.txt');\n            $this->assertEquals('new contents', $contents);\n            $visibility = $adapter->visibility('path.txt')->visibility();\n            $this->assertEquals(Visibility::PRIVATE, $visibility);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function a_file_exists_only_when_it_is_written_and_not_deleted(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n\n            // does not exist before creation\n            self::assertFalse($adapter->fileExists('path.txt'));\n\n            // a file exists after creation\n            $this->givenWeHaveAnExistingFile('path.txt');\n            self::assertTrue($adapter->fileExists('path.txt'));\n\n            // a file no longer exists after creation\n            $adapter->delete('path.txt');\n            self::assertFalse($adapter->fileExists('path.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function listing_contents_shallow(): void\n    {\n        $this->runScenario(function () {\n            $this->givenWeHaveAnExistingFile('some/0-path.txt', 'contents');\n            $this->givenWeHaveAnExistingFile('some/1-nested/path.txt', 'contents');\n\n            $listing = $this->adapter()->listContents('some', false);\n            /** @var StorageAttributes[] $items */\n            $items = iterator_to_array($listing);\n\n            $this->assertInstanceOf(Generator::class, $listing);\n            $this->assertContainsOnlyInstancesOf(StorageAttributes::class, $items);\n\n            $this->assertCount(2, $items, $this->formatIncorrectListingCount($items));\n\n            // Order of entries is not guaranteed\n            [$fileIndex, $directoryIndex] = $items[0]->isFile() ? [0, 1] : [1, 0];\n\n            $this->assertEquals('some/0-path.txt', $items[$fileIndex]->path());\n            $this->assertEquals('some/1-nested', $items[$directoryIndex]->path());\n            $this->assertTrue($items[$fileIndex]->isFile());\n            $this->assertTrue($items[$directoryIndex]->isDir());\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function checking_if_a_non_existing_directory_exists(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            self::assertFalse($adapter->directoryExists('this-does-not-exist.php'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function checking_if_a_directory_exists_after_writing_a_file(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $this->givenWeHaveAnExistingFile('existing-directory/file.txt');\n            self::assertTrue($adapter->directoryExists('existing-directory'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function checking_if_a_directory_exists_after_creating_it(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->createDirectory('explicitly-created-directory', new Config());\n            self::assertTrue($adapter->directoryExists('explicitly-created-directory'));\n            $adapter->deleteDirectory('explicitly-created-directory');\n            $l = iterator_to_array($adapter->listContents('/', false), false);\n            self::assertEquals([], $l);\n            self::assertFalse($adapter->directoryExists('explicitly-created-directory'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function listing_contents_recursive(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->createDirectory('path', new Config());\n            $adapter->write('path/file.txt', 'string', new Config());\n\n            $listing = $adapter->listContents('', true);\n            /** @var StorageAttributes[] $items */\n            $items = iterator_to_array($listing);\n            $this->assertCount(2, $items, $this->formatIncorrectListingCount($items));\n        });\n    }\n\n    protected function formatIncorrectListingCount(array $items): string\n    {\n        $message = \"Incorrect number of items returned.\\nThe listing contains:\\n\\n\";\n\n        /** @var StorageAttributes $item */\n        foreach ($items as $item) {\n            $message .= \"- {$item->path()}\\n\";\n        }\n\n        return $message . PHP_EOL;\n    }\n\n    protected function givenWeHaveAnExistingFile(string $path, string $contents = 'contents', array $config = []): void\n    {\n        $this->runSetup(function () use ($path, $contents, $config) {\n            $this->adapter()->write($path, $contents, new Config($config));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_file_size(): void\n    {\n        $adapter = $this->adapter();\n        $this->givenWeHaveAnExistingFile('path.txt', 'contents');\n\n        $this->runScenario(function () use ($adapter) {\n            $attributes = $adapter->fileSize('path.txt');\n            $this->assertInstanceOf(FileAttributes::class, $attributes);\n            $this->assertEquals(8, $attributes->fileSize());\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function setting_visibility(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $this->givenWeHaveAnExistingFile('path.txt', 'contents', [Config::OPTION_VISIBILITY => Visibility::PUBLIC]);\n\n            $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('path.txt')->visibility());\n\n            $adapter->setVisibility('path.txt', Visibility::PRIVATE);\n\n            $this->assertEquals(Visibility::PRIVATE, $adapter->visibility('path.txt')->visibility());\n\n            $adapter->setVisibility('path.txt', Visibility::PUBLIC);\n\n            $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('path.txt')->visibility());\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_file_size_of_a_directory(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n\n        $adapter = $this->adapter();\n\n        $this->runScenario(function () use ($adapter) {\n            $adapter->createDirectory('path', new Config());\n            $adapter->fileSize('path/');\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_file_size_of_non_existing_file(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->fileSize('non-existing-file.txt');\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_last_modified_of_non_existing_file(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->lastModified('non-existing-file.txt');\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_visibility_of_non_existing_file(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->visibility('non-existing-file.txt');\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_the_mime_type_of_an_svg_file(): void\n    {\n        $this->runScenario(function () {\n            $this->givenWeHaveAnExistingFile('file.svg', file_get_contents(__DIR__ . '/test_files/flysystem.svg'));\n\n            $mimetype = $this->adapter()->mimeType('file.svg')->mimeType();\n\n            $this->assertStringStartsWith('image/svg+xml', $mimetype);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_mime_type_of_non_existing_file(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->mimeType('non-existing-file.txt');\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_unknown_mime_type_of_a_file(): void\n    {\n        $this->givenWeHaveAnExistingFile(\n            'unknown-mime-type.md5',\n            file_get_contents(__DIR__ . '/test_files/unknown-mime-type.md5')\n        );\n\n        $this->expectException(UnableToRetrieveMetadata::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->mimeType('unknown-mime-type.md5');\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function listing_a_toplevel_directory(): void\n    {\n        $this->givenWeHaveAnExistingFile('path1.txt');\n        $this->givenWeHaveAnExistingFile('path2.txt');\n\n        $this->runScenario(function () {\n            $contents = iterator_to_array($this->adapter()->listContents('', true));\n\n            $this->assertCount(2, $contents);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function writing_and_reading_with_streams(): void\n    {\n        $this->runScenario(function () {\n            $writeStream = stream_with_contents('contents');\n            $adapter = $this->adapter();\n\n            $adapter->writeStream('path.txt', $writeStream, new Config());\n            if (is_resource($writeStream)) {\n                fclose($writeStream);\n            };\n            $readStream = $adapter->readStream('path.txt');\n\n            $this->assertIsResource($readStream);\n            $contents = stream_get_contents($readStream);\n            fclose($readStream);\n            $this->assertEquals('contents', $contents);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function setting_visibility_on_a_file_that_does_not_exist(): void\n    {\n        $this->expectException(UnableToSetVisibility::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->setVisibility('this-path-does-not-exists.txt', Visibility::PRIVATE);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be copied',\n                new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])\n            );\n\n            $adapter->copy('source.txt', 'destination.txt', new Config());\n\n            $this->assertTrue($adapter->fileExists('source.txt'));\n            $this->assertTrue($adapter->fileExists('destination.txt'));\n            $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('destination.txt')->visibility());\n            $this->assertEquals('text/plain', $adapter->mimeType('destination.txt')->mimeType());\n            $this->assertEquals('contents to be copied', $adapter->read('destination.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file_that_does_not_exist(): void\n    {\n        $this->expectException(UnableToCopyFile::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->copy('source.txt', 'destination.txt', new Config());\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file_again(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be copied',\n                new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])\n            );\n\n            $adapter->copy('source.txt', 'destination.txt', new Config());\n\n            $this->assertTrue($adapter->fileExists('source.txt'));\n            $this->assertTrue($adapter->fileExists('destination.txt'));\n            $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('destination.txt')->visibility());\n            $this->assertEquals('contents to be copied', $adapter->read('destination.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function moving_a_file(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be copied',\n                new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])\n            );\n            $adapter->move('source.txt', 'destination.txt', new Config());\n            $this->assertFalse(\n                $adapter->fileExists('source.txt'),\n                'After moving a file should no longer exist in the original location.'\n            );\n            $this->assertTrue(\n                $adapter->fileExists('destination.txt'),\n                'After moving, a file should be present at the new location.'\n            );\n            $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('destination.txt')->visibility());\n            $this->assertEquals('text/plain', $adapter->mimeType('destination.txt')->mimeType());\n            $this->assertEquals('contents to be copied', $adapter->read('destination.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function file_exists_on_directory_is_false(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n\n            $this->assertFalse($adapter->directoryExists('test'));\n            $adapter->createDirectory('test', new Config());\n\n            $this->assertTrue($adapter->directoryExists('test'));\n            $this->assertFalse($adapter->fileExists('test'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function directory_exists_on_file_is_false(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n\n            $this->assertFalse($adapter->fileExists('test.txt'));\n            $adapter->write('test.txt', 'content', new Config());\n\n            $this->assertTrue($adapter->fileExists('test.txt'));\n            $this->assertFalse($adapter->directoryExists('test.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function reading_a_file_that_does_not_exist(): void\n    {\n        $this->expectException(UnableToReadFile::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->read('path.txt');\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function moving_a_file_that_does_not_exist(): void\n    {\n        $this->expectException(UnableToMoveFile::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->move('source.txt', 'destination.txt', new Config());\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function trying_to_delete_a_non_existing_file(): void\n    {\n        $adapter = $this->adapter();\n\n        $adapter->delete('path.txt');\n        $fileExists = $adapter->fileExists('path.txt');\n\n        $this->assertFalse($fileExists);\n    }\n\n    /**\n     * @test\n     */\n    public function checking_if_files_exist(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $fileExistsBefore = $adapter->fileExists('some/path.txt');\n            $adapter->write('some/path.txt', 'contents', new Config());\n            $fileExistsAfter = $adapter->fileExists('some/path.txt');\n\n            $this->assertFalse($fileExistsBefore);\n            $this->assertTrue($fileExistsAfter);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_last_modified(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write('path.txt', 'contents', new Config());\n\n            $attributes = $adapter->lastModified('path.txt');\n\n            $this->assertInstanceOf(FileAttributes::class, $attributes);\n            $this->assertIsInt($attributes->lastModified());\n            $this->assertTrue($attributes->lastModified() > time() - 30);\n            $this->assertTrue($attributes->lastModified() < time() + 30);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_read_a_non_existing_file_into_a_stream(): void\n    {\n        $this->expectException(UnableToReadFile::class);\n\n        $this->adapter()->readStream('something.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_read_a_non_existing_file(): void\n    {\n        $this->expectException(UnableToReadFile::class);\n\n        $this->adapter()->read('something.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function creating_a_directory(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n\n            $adapter->createDirectory('creating_a_directory/path', new Config());\n\n            // Creating a directory should be idempotent.\n            $adapter->createDirectory('creating_a_directory/path', new Config());\n\n            $contents = iterator_to_array($adapter->listContents('creating_a_directory', false));\n            $this->assertCount(1, $contents, $this->formatIncorrectListingCount($contents));\n            /** @var DirectoryAttributes $directory */\n            $directory = $contents[0];\n            $this->assertInstanceOf(DirectoryAttributes::class, $directory);\n            $this->assertEquals('creating_a_directory/path', $directory->path());\n            $adapter->deleteDirectory('creating_a_directory/path');\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file_with_collision(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write('path.txt', 'new contents', new Config());\n            $adapter->write('new-path.txt', 'contents', new Config());\n\n            $adapter->copy('path.txt', 'new-path.txt', new Config());\n            $contents = $adapter->read('new-path.txt');\n\n            $this->assertEquals('new contents', $contents);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function moving_a_file_with_collision(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write('path.txt', 'new contents', new Config());\n            $adapter->write('new-path.txt', 'contents', new Config());\n\n            $adapter->move('path.txt', 'new-path.txt', new Config());\n\n            $oldFileExists = $adapter->fileExists('path.txt');\n            $this->assertFalse($oldFileExists);\n\n            $contents = $adapter->read('new-path.txt');\n            $this->assertEquals('new contents', $contents);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file_with_same_destination(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write('path.txt', 'new contents', new Config());\n\n            $adapter->copy('path.txt', 'path.txt', new Config());\n            $contents = $adapter->read('path.txt');\n\n            $this->assertEquals('new contents', $contents);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function moving_a_file_with_same_destination(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write('path.txt', 'new contents', new Config());\n\n            $adapter->move('path.txt', 'path.txt', new Config());\n\n            $contents = $adapter->read('path.txt');\n            $this->assertEquals('new contents', $contents);\n        });\n    }\n\n    protected function assertFileExistsAtPath(string $path): void\n    {\n        $this->runScenario(function () use ($path) {\n            $fileExists = $this->adapter()->fileExists($path);\n            $this->assertTrue($fileExists);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function generating_a_public_url(): void\n    {\n        $adapter = $this->adapter();\n\n        if ( ! $adapter instanceof PublicUrlGenerator) {\n            $this->markTestSkipped('Adapter does not supply public URls');\n        }\n\n        $adapter->write('some/path.txt', 'public contents', new Config(['visibility' => 'public']));\n\n        $url = $adapter->publicUrl('some/path.txt', new Config());\n        $contents = file_get_contents($url);\n\n        self::assertEquals('public contents', $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function generating_a_temporary_url(): void\n    {\n        $adapter = $this->adapter();\n\n        if ( ! $adapter instanceof TemporaryUrlGenerator) {\n            $this->markTestSkipped('Adapter does not supply temporary URls');\n        }\n\n        $adapter->write('some/private.txt', 'public contents', new Config(['visibility' => 'private']));\n\n        $expiresAt = (new DateTimeImmutable())->add(DateInterval::createFromDateString('1 minute'));\n        $url = $adapter->temporaryUrl('some/private.txt', $expiresAt, new Config());\n        $contents = file_get_contents($url);\n\n        self::assertEquals('public contents', $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function get_checksum(): void\n    {\n        $adapter = $this->adapter();\n\n        if ( ! $adapter instanceof ChecksumProvider) {\n            $this->markTestSkipped('Adapter does not supply providing checksums');\n        }\n\n        $adapter->write('path.txt', 'foobar', new Config());\n\n        $this->assertSame('3858f62230ac3c915f300c664312c63f', $adapter->checksum('path.txt', new Config()));\n    }\n\n    /**\n     * @test\n     */\n    public function cannot_get_checksum_for_non_existent_file(): void\n    {\n        $adapter = $this->adapter();\n\n        if ( ! $adapter instanceof ChecksumProvider) {\n            $this->markTestSkipped('Adapter does not supply providing checksums');\n        }\n\n        $this->expectException(UnableToProvideChecksum::class);\n\n        $adapter->checksum('path.txt', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function cannot_get_checksum_for_directory(): void\n    {\n        $adapter = $this->adapter();\n\n        if ( ! $adapter instanceof ChecksumProvider) {\n            $this->markTestSkipped('Adapter does not supply providing checksums');\n        }\n\n        $adapter->createDirectory('dir', new Config());\n\n        $this->expectException(UnableToProvideChecksum::class);\n\n        $adapter->checksum('dir', new Config());\n    }\n}\n"
  },
  {
    "path": "src/AdapterTestUtilities/README.md",
    "content": "## Flysystem Adapter Test Utilities\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n\nRequire this package to make use of some adapter test utilities.\n\n```bash\ncomposer require --dev league/flysystem-adapter-test-utilities\n```\n\nView the [documentation of Flysystem](https://flysystem.thephpleague.com/docs/).\n"
  },
  {
    "path": "src/AdapterTestUtilities/RetryOnTestException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AdapterTestUtilities;\n\nuse const PHP_EOL;\nuse const STDOUT;\nuse League\\Flysystem\\FilesystemException;\nuse Throwable;\n\n/**\n * @codeCoverageIgnore\n */\ntrait RetryOnTestException\n{\n    /**\n     * @var string\n     */\n    protected $exceptionTypeToRetryOn;\n\n    /**\n     * @var int\n     */\n    protected $timeoutForExceptionRetry = 2;\n\n    protected function retryOnException(string $className, int $timout = 2): void\n    {\n        $this->exceptionTypeToRetryOn = $className;\n        $this->timeoutForExceptionRetry = $timout;\n    }\n\n    protected function retryScenarioOnException(string $className, callable $scenario, int $timeout = 2): void\n    {\n        $this->retryOnException($className, $timeout);\n        $this->runScenario($scenario);\n    }\n\n    protected function dontRetryOnException(): void\n    {\n        $this->exceptionTypeToRetryOn = null;\n    }\n\n    /**\n     * @internal\n     *\n     * @throws Throwable\n     */\n    protected function runSetup(callable $scenario): void\n    {\n        $previousException = $this->exceptionTypeToRetryOn;\n        $previousTimeout = $this->timeoutForExceptionRetry;\n        $this->retryOnException(FilesystemException::class);\n\n        try {\n            $this->runScenario($scenario);\n        } finally {\n            $this->exceptionTypeToRetryOn = $previousException;\n            $this->timeoutForExceptionRetry = $previousTimeout;\n        }\n    }\n\n    protected function runScenario(callable $scenario): void\n    {\n        if ($this->exceptionTypeToRetryOn === null) {\n            $scenario();\n\n            return;\n        }\n\n        $firstTryAt = \\time();\n        $lastTryAt = $firstTryAt + 60;\n\n        while (time() <= $lastTryAt) {\n            try {\n                $scenario();\n\n                return;\n            } catch (Throwable $exception) {\n                if ( ! $exception instanceof $this->exceptionTypeToRetryOn) {\n                    throw $exception;\n                }\n                fwrite(STDOUT, 'Retrying ...' . PHP_EOL);\n                sleep($this->timeoutForExceptionRetry);\n            }\n        }\n\n        $this->exceptionTypeToRetryOn = null;\n\n        if (isset($exception) && $exception instanceof Throwable) {\n            throw $exception;\n        }\n    }\n}\n"
  },
  {
    "path": "src/AdapterTestUtilities/ToxiproxyManagement.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AdapterTestUtilities;\n\nuse GuzzleHttp\\Client;\n\n/**\n * This class provides a client for the HTTP API provided by the proxy that simulates network issues.\n *\n * @see https://github.com/shopify/toxiproxy#http-api\n *\n * @phpstan-type RegisteredProxies 'ftp'|'sftp'|'ftpd'\n * @phpstan-type StreamDirection 'upstream'|'downstream'\n * @phpstan-type Type 'latency'|'bandwidth'|'slow_close'|'timeout'|'reset_peer'|'slicer'|'limit_data'\n * @phpstan-type Attributes array{latency?: int, jitter?: int, rate?: int, delay?: int}\n * @phpstan-type Toxic array{name?: string, type: Type, stream?: StreamDirection, toxicity?: float, attributes: Attributes}\n */\nfinal class ToxiproxyManagement\n{\n    /** @var Client */\n    private $apiClient;\n\n    public function __construct(Client $apiClient)\n    {\n        $this->apiClient = $apiClient;\n    }\n\n    public static function forServer(string $apiUri = 'http://localhost:8474'): self\n    {\n        return new self(\n            new Client(\n                [\n                    'base_uri' => $apiUri,\n                    'base_url' => $apiUri, // Compatibility with older versions of Guzzle\n                ]\n            )\n        );\n    }\n\n    public function removeAllToxics(): void\n    {\n        $this->apiClient->post('/reset');\n    }\n\n    /**\n     * Simulates a peer reset on the client->server direction.\n     *\n     * @param RegisteredProxies $proxyName\n     */\n    public function resetPeerOnRequest(\n        string $proxyName,\n        int $timeoutInMilliseconds\n    ): void {\n        $configuration = [\n            'type' => 'reset_peer',\n            'stream' => 'upstream',\n            'attributes' => ['timeout' => $timeoutInMilliseconds],\n        ];\n\n        $this->addToxic($proxyName, $configuration);\n    }\n\n    /**\n     * Registers a network toxic for the given proxy.\n     *\n     * @param RegisteredProxies $proxyName\n     * @param Toxic $configuration\n     */\n    private function addToxic(string $proxyName, array $configuration): void\n    {\n        $this->apiClient->post('/proxies/' . $proxyName . '/toxics', ['json' => $configuration]);\n    }\n}\n"
  },
  {
    "path": "src/AdapterTestUtilities/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-adapter-test-utilities\",\n    \"description\": \"Flysystem utilities for testing adapters.\",\n    \"keywords\": [\"filesystem\", \"flysystem\", \"adapter\", \"test\", \"utilities\"],\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"autoload\": {\n        \"psr-4\": {\n            \"League\\\\Flysystem\\\\AdapterTestUtilities\\\\\": \"\"\n        },\n        \"files\": [\n            \"test-functions.php\"\n        ]\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"league/flysystem\": \"^3.0.0\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ]\n}\n"
  },
  {
    "path": "src/AdapterTestUtilities/test-functions.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nfunction return_mocked_value(string $name)\n{\n    return array_shift($_ENV['__FM:RETURNS:' . $name]);\n}\n\nfunction reset_function_mocks()\n{\n    foreach (array_keys($_ENV) as $name) {\n        if (is_string($name) && substr($name, 0, 5) === '__FM:') {\n            unset($_ENV[$name]);\n        }\n    }\n}\n\nfunction mock_function(string $name, ...$returns)\n{\n    $_ENV['__FM:FUNC_IS_MOCKED:' . $name] = 'yes';\n    $_ENV['__FM:RETURNS:' . $name] = $returns;\n}\n\nfunction is_mocked(string $name)\n{\n    return ($_ENV['__FM:FUNC_IS_MOCKED:' . $name] ?? 'no') === 'yes';\n}\n\nfunction stream_with_contents(string $contents)\n{\n    $stream = fopen('php://temp', 'w+b');\n    fwrite($stream, $contents);\n    rewind($stream);\n\n    return $stream;\n}\n\nfunction delete_directory(string $dir): void\n{\n    if ( ! is_dir($dir)) {\n        return;\n    }\n\n    foreach ((array) scandir($dir) as $file) {\n        if ('.' === $file || '..' === $file) {\n            continue;\n        }\n        if (is_dir(\"$dir/$file\")) {\n            delete_directory(\"$dir/$file\");\n        } else {\n            unlink(\"$dir/$file\");\n        }\n    }\n    rmdir($dir);\n}\n"
  },
  {
    "path": "src/AdapterTestUtilities/test_files/unknown-mime-type.md5",
    "content": "141d15ed35fc57dcc3c72bba881742b1"
  },
  {
    "path": "src/AsyncAwsS3/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n**/*Test.php export-ignore\n**/*Stub.php export-ignore\nREADME.md export-ignore\n"
  },
  {
    "path": "src/AsyncAwsS3/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/AsyncAwsS3/AsyncAwsS3Adapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AsyncAwsS3;\n\nuse AsyncAws\\Core\\Exception\\Http\\ClientException;\nuse AsyncAws\\Core\\Stream\\ResultStream;\nuse AsyncAws\\S3\\Input\\GetObjectRequest;\nuse AsyncAws\\S3\\Result\\HeadObjectOutput;\nuse AsyncAws\\S3\\S3Client;\nuse AsyncAws\\S3\\ValueObject\\AwsObject;\nuse AsyncAws\\S3\\ValueObject\\CommonPrefix;\nuse AsyncAws\\S3\\ValueObject\\ObjectIdentifier;\nuse AsyncAws\\SimpleS3\\SimpleS3Client;\nuse DateTimeImmutable;\nuse DateTimeInterface;\nuse Generator;\nuse League\\Flysystem\\ChecksumAlgoIsNotSupported;\nuse League\\Flysystem\\ChecksumProvider;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\StorageAttributes;\nuse League\\Flysystem\\UnableToCheckDirectoryExistence;\nuse League\\Flysystem\\UnableToCheckFileExistence;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToGeneratePublicUrl;\nuse League\\Flysystem\\UnableToGenerateTemporaryUrl;\nuse League\\Flysystem\\UnableToListContents;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToProvideChecksum;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UrlGeneration\\PublicUrlGenerator;\nuse League\\Flysystem\\UrlGeneration\\TemporaryUrlGenerator;\nuse League\\Flysystem\\Visibility;\nuse League\\MimeTypeDetection\\FinfoMimeTypeDetector;\nuse League\\MimeTypeDetection\\MimeTypeDetector;\nuse Throwable;\nuse function trim;\n\nclass AsyncAwsS3Adapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider, TemporaryUrlGenerator\n{\n    /**\n     * @var string[]\n     */\n    public const AVAILABLE_OPTIONS = [\n        'ACL',\n        'CacheControl',\n        'ContentDisposition',\n        'ContentEncoding',\n        'ContentLength',\n        'ContentType',\n        'ContentMD5',\n        'Expires',\n        'GrantFullControl',\n        'GrantRead',\n        'GrantReadACP',\n        'GrantWriteACP',\n        'Metadata',\n        'MetadataDirective',\n        'RequestPayer',\n        'SSECustomerAlgorithm',\n        'SSECustomerKey',\n        'SSECustomerKeyMD5',\n        'SSEKMSKeyId',\n        'ServerSideEncryption',\n        'StorageClass',\n        'Tagging',\n        'WebsiteRedirectLocation',\n        'ChecksumAlgorithm',\n        'CopySourceSSECustomerAlgorithm',\n        'CopySourceSSECustomerKey',\n        'CopySourceSSECustomerKeyMD5',\n    ];\n\n    /**\n     * @var string[]\n     */\n    protected const EXTRA_METADATA_FIELDS = [\n        'Metadata',\n        'StorageClass',\n        'ETag',\n        'VersionId',\n    ];\n\n    private PathPrefixer $prefixer;\n    private VisibilityConverter $visibility;\n    private MimeTypeDetector $mimeTypeDetector;\n\n    /**\n     * @var array|string[]\n     */\n    private array $forwardedOptions;\n\n    /**\n     * @var array|string[]\n     */\n    private array $metadataFields;\n\n    /**\n     * @param S3Client|SimpleS3Client $client Uploading of files larger than 5GB is only supported with SimpleS3Client\n     */\n    public function __construct(\n        private S3Client $client,\n        private string $bucket,\n        string $prefix = '',\n        ?VisibilityConverter $visibility = null,\n        ?MimeTypeDetector $mimeTypeDetector = null,\n        array $forwardedOptions = self::AVAILABLE_OPTIONS,\n        array $metadataFields = self::EXTRA_METADATA_FIELDS,\n    ) {\n        $this->prefixer = new PathPrefixer($prefix);\n        $this->visibility = $visibility ?? new PortableVisibilityConverter();\n        $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();\n        $this->forwardedOptions = $forwardedOptions;\n        $this->metadataFields = $metadataFields;\n    }\n\n    public function fileExists(string $path): bool\n    {\n        try {\n            return $this->client->objectExists(\n                [\n                    'Bucket' => $this->bucket,\n                    'Key' => $this->prefixer->prefixPath($path),\n                ]\n            )->isSuccess();\n        } catch (ClientException $e) {\n            throw UnableToCheckFileExistence::forLocation($path, $e);\n        }\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        $this->upload($path, $contents, $config);\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        $this->upload($path, $contents, $config);\n    }\n\n    public function read(string $path): string\n    {\n        $body = $this->readObject($path);\n\n        return $body->getContentAsString();\n    }\n\n    public function readStream(string $path)\n    {\n        $body = $this->readObject($path);\n\n        return $body->getContentAsResource();\n    }\n\n    public function delete(string $path): void\n    {\n        $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];\n\n        try {\n            $this->client->deleteObject($arguments);\n        } catch (Throwable $exception) {\n            throw UnableToDeleteFile::atLocation($path, '', $exception);\n        }\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        $prefix = $this->prefixer->prefixDirectoryPath($path);\n        $prefix = ltrim($prefix, '/');\n\n        $objects = [];\n        $params = ['Bucket' => $this->bucket, 'Prefix' => $prefix];\n\n        try {\n            $result = $this->client->listObjectsV2($params);\n            /** @var AwsObject $item */\n            foreach ($result->getContents() as $item) {\n                $key = $item->getKey();\n                if (null !== $key) {\n                    $objects[] = $this->createObjectIdentifierForXmlRequest($key);\n                }\n            }\n\n            if (empty($objects)) {\n                return;\n            }\n\n            foreach (array_chunk($objects, 1000) as $chunk) {\n                $this->client->deleteObjects([\n                    'Bucket' => $this->bucket,\n                    'Delete' => ['Objects' => $chunk],\n                ]);\n            }\n        } catch (\\Throwable $e) {\n            throw UnableToDeleteDirectory::atLocation($path, $e->getMessage(), $e);\n        }\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        $defaultVisibility = $config->get(Config::OPTION_DIRECTORY_VISIBILITY, $this->visibility->defaultForDirectories());\n        $config = $config->withDefaults([Config::OPTION_VISIBILITY => $defaultVisibility]);\n\n        try {\n            $this->upload(rtrim($path, '/') . '/', '', $config);\n        } catch (Throwable $e) {\n            throw UnableToCreateDirectory::dueToFailure($path, $e);\n        }\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        $arguments = [\n            'Bucket' => $this->bucket,\n            'Key' => $this->prefixer->prefixPath($path),\n            'ACL' => $this->visibility->visibilityToAcl($visibility),\n        ];\n\n        try {\n            $this->client->putObjectAcl($arguments);\n        } catch (Throwable $exception) {\n            throw UnableToSetVisibility::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];\n\n        try {\n            $result = $this->client->getObjectAcl($arguments);\n            $grants = $result->getGrants();\n        } catch (Throwable $exception) {\n            throw UnableToRetrieveMetadata::visibility($path, $exception->getMessage(), $exception);\n        }\n\n        $visibility = $this->visibility->aclToVisibility($grants);\n\n        return new FileAttributes($path, null, $visibility);\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        $attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_MIME_TYPE);\n\n        if (null === $attributes->mimeType()) {\n            throw UnableToRetrieveMetadata::mimeType($path);\n        }\n\n        return $attributes;\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        $attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_LAST_MODIFIED);\n\n        if (null === $attributes->lastModified()) {\n            throw UnableToRetrieveMetadata::lastModified($path);\n        }\n\n        return $attributes;\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        $attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_FILE_SIZE);\n\n        if (null === $attributes->fileSize()) {\n            throw UnableToRetrieveMetadata::fileSize($path);\n        }\n\n        return $attributes;\n    }\n\n    public function directoryExists(string $path): bool\n    {\n        try {\n            $prefix = $this->prefixer->prefixDirectoryPath($path);\n            $options = ['Bucket' => $this->bucket, 'Prefix' => $prefix, 'MaxKeys' => 1, 'Delimiter' => '/'];\n\n            return $this->client->listObjectsV2($options)->getKeyCount() > 0;\n        } catch (Throwable $exception) {\n            throw UnableToCheckDirectoryExistence::forLocation($path, $exception);\n        }\n    }\n\n    public function listContents(string $path, bool $deep): iterable\n    {\n        $path = trim($path, '/');\n        $prefix = trim($this->prefixer->prefixPath($path), '/');\n        $prefix = $prefix === '' ? '' : $prefix . '/';\n        $options = ['Bucket' => $this->bucket, 'Prefix' => $prefix];\n\n        if (false === $deep) {\n            $options['Delimiter'] = '/';\n        }\n\n        try {\n            $listing = $this->retrievePaginatedListing($options);\n\n            foreach ($listing as $item) {\n                $item = $this->mapS3ObjectMetadata($item);\n\n                if ($item->path() === $path) {\n                    continue;\n                }\n\n                yield $item;\n            }\n        } catch (\\Throwable $e) {\n            throw UnableToListContents::atLocation($path, $deep, $e);\n        }\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        if ($source === $destination) {\n            return;\n        }\n\n        try {\n            $this->copy($source, $destination, $config);\n            $this->delete($source);\n        } catch (Throwable $exception) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        if ($source === $destination) {\n            return;\n        }\n\n        try {\n            $visibility = $config->get(Config::OPTION_VISIBILITY);\n\n            if ($visibility === null && $config->get(Config::OPTION_RETAIN_VISIBILITY, true)) {\n                $visibility = $this->visibility($source)->visibility();\n            }\n        } catch (Throwable $exception) {\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);\n        }\n\n        $arguments = [\n            'ACL' => $this->visibility->visibilityToAcl($visibility ?: 'private'),\n            'Bucket' => $this->bucket,\n            'Key' => $this->prefixer->prefixPath($destination),\n            'CopySource' => rawurlencode($this->bucket . '/' . $this->prefixer->prefixPath($source)),\n        ];\n\n        try {\n            $this->client->copyObject($arguments);\n        } catch (Throwable $exception) {\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    /**\n     * @param string|resource $body\n     */\n    private function upload(string $path, $body, Config $config): void\n    {\n        $key = $this->prefixer->prefixPath($path);\n        $acl = $this->determineAcl($config);\n        $options = $this->createOptionsFromConfig($config);\n        $shouldDetermineMimetype = '' !== $body && ! \\array_key_exists('ContentType', $options);\n\n        if ($shouldDetermineMimetype && $mimeType = $this->mimeTypeDetector->detectMimeType($key, $body)) {\n            $options['ContentType'] = $mimeType;\n        }\n\n        try {\n            if ($this->client instanceof SimpleS3Client) {\n                // Supports upload of files larger than 5GB\n                $this->client->upload($this->bucket, $key, $body, array_merge($options, ['ACL' => $acl]));\n            } else {\n                $this->client->putObject(array_merge($options, [\n                    'Bucket' => $this->bucket,\n                    'Key' => $key,\n                    'Body' => $body,\n                    'ACL' => $acl,\n                ]));\n            }\n        } catch (Throwable $exception) {\n            throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    private function determineAcl(Config $config): string\n    {\n        $visibility = (string) $config->get(Config::OPTION_VISIBILITY, Visibility::PRIVATE);\n\n        return $this->visibility->visibilityToAcl($visibility);\n    }\n\n    private function createOptionsFromConfig(Config $config): array\n    {\n        $options = [];\n\n        foreach ($this->forwardedOptions as $option) {\n            $value = $config->get($option, '__NOT_SET__');\n\n            if ('__NOT_SET__' !== $value) {\n                $options[$option] = $value;\n            }\n        }\n\n        return $options;\n    }\n\n    private function fetchFileMetadata(string $path, string $type): FileAttributes\n    {\n        $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];\n\n        try {\n            $result = $this->client->headObject($arguments);\n            $result->resolve();\n        } catch (Throwable $exception) {\n            throw UnableToRetrieveMetadata::create($path, $type, $exception->getMessage(), $exception);\n        }\n\n        $attributes = $this->mapS3ObjectMetadata($result, $path);\n\n        if ( ! $attributes instanceof FileAttributes) {\n            throw UnableToRetrieveMetadata::create($path, $type, 'Unable to retrieve file attributes, directory attributes received.');\n        }\n\n        return $attributes;\n    }\n\n    /**\n     * @param HeadObjectOutput|AwsObject|CommonPrefix $item\n     */\n    private function mapS3ObjectMetadata($item, ?string $path = null): StorageAttributes\n    {\n        if (null === $path) {\n            if ($item instanceof AwsObject) {\n                $path = $this->prefixer->stripPrefix($item->getKey() ?? '');\n            } elseif ($item instanceof CommonPrefix) {\n                $path = $this->prefixer->stripPrefix($item->getPrefix() ?? '');\n            } else {\n                throw new \\RuntimeException(sprintf('Argument 2 of \"%s\" cannot be null when $item is not instance of \"%s\" or %s', __METHOD__, AwsObject::class, CommonPrefix::class));\n            }\n        }\n\n        if ('/' === substr($path, -1)) {\n            return new DirectoryAttributes(rtrim($path, '/'));\n        }\n\n        $mimeType = null;\n        $fileSize = null;\n        $lastModified = null;\n        $dateTime = null;\n        $metadata = [];\n\n        if ($item instanceof AwsObject) {\n            $dateTime = $item->getLastModified();\n            $fileSize = $item->getSize();\n        } elseif ($item instanceof CommonPrefix) {\n            // No data available\n        } elseif ($item instanceof HeadObjectOutput) {\n            $mimeType = $item->getContentType();\n            $fileSize = $item->getContentLength();\n            $dateTime = $item->getLastModified();\n            $metadata = $this->extractExtraMetadata($item);\n        } else {\n            throw new \\RuntimeException(sprintf('Object of class \"%s\" is not supported in %s()', \\get_class($item), __METHOD__));\n        }\n\n        if ($dateTime instanceof \\DateTimeInterface) {\n            $lastModified = $dateTime->getTimestamp();\n        }\n\n        return new FileAttributes($path, $fileSize !== null ? (int) $fileSize : null, null, $lastModified, $mimeType, $metadata);\n    }\n\n    /**\n     * @param HeadObjectOutput $metadata\n     */\n    private function extractExtraMetadata($metadata): array\n    {\n        $extracted = [];\n\n        foreach ($this->metadataFields as $field) {\n            $method = 'get' . $field;\n            if ( ! method_exists($metadata, $method)) {\n                continue;\n            }\n            $value = $metadata->$method();\n            if (null !== $value) {\n                $extracted[$field] = $value;\n            }\n        }\n\n        return $extracted;\n    }\n\n    private function retrievePaginatedListing(array $options): Generator\n    {\n        $result = $this->client->listObjectsV2($options);\n\n        foreach ($result as $item) {\n            yield $item;\n        }\n    }\n\n    private function readObject(string $path): ResultStream\n    {\n        $options = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];\n\n        try {\n            return $this->client->getObject($options)->getBody();\n        } catch (Throwable $exception) {\n            throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    private function createObjectIdentifierForXmlRequest(string $key): ObjectIdentifier\n    {\n        $escapedKey = htmlentities($key, ENT_XML1 | ENT_QUOTES, 'UTF-8');\n\n        if ($escapedKey === '') {\n            throw new \\RuntimeException(sprintf('Cannot escape key \"%s\" for XML request, htmlentities() returned an empty string.', $key));\n        }\n\n        return new ObjectIdentifier(['Key' => $escapedKey]);\n    }\n\n    public function publicUrl(string $path, Config $config): string\n    {\n        if ( ! $this->client instanceof SimpleS3Client) {\n            throw UnableToGeneratePublicUrl::noGeneratorConfigured($path, 'Client needs to be instance of SimpleS3Client');\n        }\n\n        try {\n            return $this->client->getUrl($this->bucket, $this->prefixer->prefixPath($path));\n        } catch (Throwable $exception) {\n            throw UnableToGeneratePublicUrl::dueToError($path, $exception);\n        }\n    }\n\n    public function checksum(string $path, Config $config): string\n    {\n        $algo = $config->get('checksum_algo', 'etag');\n\n        if ($algo !== 'etag') {\n            throw new ChecksumAlgoIsNotSupported();\n        }\n\n        try {\n            $metadata = $this->fetchFileMetadata($path, 'checksum')->extraMetadata();\n        } catch (UnableToRetrieveMetadata $exception) {\n            throw new UnableToProvideChecksum($exception->reason(), $path, $exception);\n        }\n\n        if ( ! isset($metadata['ETag'])) {\n            throw new UnableToProvideChecksum('ETag header not available.', $path);\n        }\n\n        return trim($metadata['ETag'], '\"');\n    }\n\n    public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string\n    {\n        try {\n            $request = new GetObjectRequest([\n                'Bucket' => $this->bucket,\n                'Key' => $this->prefixer->prefixPath($path),\n            ] + $config->get('get_object_options', []));\n\n            return $this->client->presign($request, DateTimeImmutable::createFromInterface($expiresAt));\n        } catch (Throwable $exception) {\n            throw UnableToGenerateTemporaryUrl::dueToError($path, $exception);\n        }\n    }\n}\n"
  },
  {
    "path": "src/AsyncAwsS3/AsyncAwsS3AdapterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AsyncAwsS3;\n\nuse AsyncAws\\Core\\Exception\\Http\\ClientException;\nuse AsyncAws\\Core\\Exception\\Http\\NetworkException;\nuse AsyncAws\\Core\\Test\\Http\\SimpleMockedResponse;\nuse AsyncAws\\Core\\Test\\ResultMockFactory;\nuse AsyncAws\\S3\\Result\\HeadObjectOutput;\nuse AsyncAws\\S3\\Result\\ListObjectsV2Output;\nuse AsyncAws\\S3\\Result\\PutObjectOutput;\nuse AsyncAws\\S3\\S3Client;\nuse AsyncAws\\S3\\ValueObject\\AwsObject;\nuse AsyncAws\\SimpleS3\\SimpleS3Client;\nuse Exception;\nuse League\\Flysystem\\AdapterTestUtilities\\FilesystemAdapterTestCase;\nuse League\\Flysystem\\AwsS3V3\\AwsS3V3Adapter;\nuse League\\Flysystem\\ChecksumAlgoIsNotSupported;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\StorageAttributes;\nuse League\\Flysystem\\UnableToCheckFileExistence;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToListContents;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\Visibility;\nuse function getenv;\nuse function iterator_to_array;\n\n/**\n * @group aws\n */\nclass AsyncAwsS3AdapterTest extends FilesystemAdapterTestCase\n{\n    /**\n     * @var bool\n     */\n    private $shouldCleanUp = false;\n\n    /**\n     * @var string\n     */\n    private static $adapterPrefix = 'test-prefix';\n\n    /**\n     * @var S3Client|null\n     */\n    private static $s3Client;\n\n    /**\n     * @var S3ClientStub\n     */\n    private static $stubS3Client;\n\n    private static function awsConfig(): array\n    {\n        $key = getenv('FLYSYSTEM_AWS_S3_KEY');\n        $secret = getenv('FLYSYSTEM_AWS_S3_SECRET');\n        $region = getenv('FLYSYSTEM_AWS_S3_REGION') ?: 'eu-central-1';\n\n        if ( ! $key || ! $secret) {\n            self::markTestSkipped('No AWS credentials present for testing.');\n        }\n\n        return [\n            'accessKeyId' => $key,\n            'accessKeySecret' => $secret,\n            'region' => $region,\n        ];\n    }\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this->retryOnException(NetworkException::class);\n    }\n\n    public static function setUpBeforeClass(): void\n    {\n        static::$adapterPrefix = 'ci/' . bin2hex(random_bytes(10));\n    }\n\n    protected function tearDown(): void\n    {\n        if ( ! $this->shouldCleanUp) {\n            return;\n        }\n\n        $adapter = $this->adapter();\n        $adapter->deleteDirectory('/');\n        /** @var StorageAttributes[] $listing */\n        $listing = $adapter->listContents('', false);\n\n        foreach ($listing as $item) {\n            if ($item->isFile()) {\n                $adapter->delete($item->path());\n            } else {\n                $adapter->deleteDirectory($item->path());\n            }\n        }\n    }\n\n    private static function s3Client(): S3Client\n    {\n        if (static::$s3Client instanceof S3Client) {\n            return static::$s3Client;\n        }\n\n        $bucket = getenv('FLYSYSTEM_AWS_S3_BUCKET');\n\n        if ( ! $bucket) {\n            self::markTestSkipped('No AWS credentials present for testing.');\n        }\n\n        static::$s3Client = new SimpleS3Client(self::awsConfig());\n\n        return static::$s3Client;\n    }\n\n    /**\n     * @test\n     */\n    public function specifying_a_custom_checksum_algo_is_not_supported(): void\n    {\n        /** @var AwsS3V3Adapter $adapter */\n        $adapter = $this->adapter();\n\n        $this->expectException(ChecksumAlgoIsNotSupported::class);\n\n        $adapter->checksum('something', new Config(['checksum_algo' => 'md5']));\n    }\n\n    /**\n     * @test\n     *\n     * @see https://github.com/thephpleague/flysystem-aws-s3-v3/issues/287\n     */\n    public function issue_287(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('KmFVvKqo/QLMExy2U/620ff60c8a154.pdf', 'pdf content', new Config());\n\n        self::assertTrue($adapter->directoryExists('KmFVvKqo'));\n    }\n\n    /**\n     * @test\n     */\n    public function writing_with_a_specific_mime_type(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('some/path.txt', 'contents', new Config(['ContentType' => 'text/plain+special']));\n        $mimeType = $adapter->mimeType('some/path.txt')->mimeType();\n        $this->assertEquals('text/plain+special', $mimeType);\n    }\n\n    /**\n     * @test\n     */\n    public function listing_contents_recursive(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('something/0/here.txt', 'contents', new Config());\n        $adapter->write('something/1/also/here.txt', 'contents', new Config());\n\n        $contents = iterator_to_array($adapter->listContents('', true));\n\n        $this->assertCount(2, $contents);\n        $this->assertContainsOnlyInstancesOf(FileAttributes::class, $contents);\n        /** @var FileAttributes $file */\n        $file = $contents[0];\n        $this->assertEquals('something/0/here.txt', $file->path());\n        /** @var FileAttributes $file */\n        $file = $contents[1];\n        $this->assertEquals('something/1/also/here.txt', $file->path());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_delete_while_moving(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('source.txt', 'contents to be copied', new Config());\n        static::$stubS3Client->throwExceptionWhenExecutingCommand('CopyObject');\n\n        $this->expectException(UnableToMoveFile::class);\n\n        $adapter->move('source.txt', 'destination.txt', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_delete_a_file(): void\n    {\n        $adapter = $this->adapter();\n        static::$stubS3Client->throwExceptionWhenExecutingCommand('DeleteObject');\n\n        $this->expectException(UnableToDeleteFile::class);\n\n        $adapter->delete('path.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function delete_directory_replaces_special_characters_by_xml_entity_codes(): void\n    {\n        $this->runScenario(function () {\n            $directory = 'to-delete';\n            $object = sprintf('/%s/\\'\\\"&<>.txt', $directory);\n\n            $adapter = $this->adapter();\n            $adapter->write(\n                $object,\n                '',\n                new Config()\n            );\n\n            $adapter->deleteDirectory($directory);\n\n            $this->assertFalse($adapter->fileExists($object));\n            $this->assertFalse($adapter->directoryExists($directory));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function delete_directory_throws_exception_if_object_key_can_not_be_escaped_correctly(): void\n    {\n        $listObjectsMock = $this->getMockBuilder(ListObjectsV2Output::class)\n            ->disableOriginalConstructor()\n            ->onlyMethods(['getContents'])\n            ->getMock();\n\n        $listObjectsMock->expects(self::once())\n            ->method('getContents')\n            ->willReturn([new AwsObject(['Key' => \"\\x8F.txt\"])]);\n\n        $s3Client = $this->getMockBuilder(S3Client::class)\n            ->disableOriginalConstructor()\n            ->onlyMethods(['ListObjectsV2'])\n            ->getMock();\n\n        $s3Client->expects(self::once())\n            ->method('ListObjectsV2')\n            ->willReturn($listObjectsMock);\n\n        $filesystem = new AsyncAwsS3Adapter($s3Client, 'my-bucket');\n\n        $this->expectException(UnableToDeleteDirectory::class);\n        $this->expectExceptionMessageMatches('/htmlentities\\(\\) returned an empty string/');\n\n        $filesystem->deleteDirectory('directory/containing/objects/with/un-escapable/key');\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_unknown_mime_type_of_a_file(): void\n    {\n        $this->adapter();\n        $result = ResultMockFactory::create(HeadObjectOutput::class, []);\n        static::$stubS3Client->stageResultForCommand('HeadObject', $result);\n\n        parent::fetching_unknown_mime_type_of_a_file();\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider dpFailingMetadataGetters\n     */\n    public function failing_to_retrieve_metadata(Exception $exception, string $getterName): void\n    {\n        $adapter = $this->adapter();\n        $result = ResultMockFactory::create(HeadObjectOutput::class, []);\n        static::$stubS3Client->stageResultForCommand('HeadObject', $result);\n\n        $this->expectExceptionObject($exception);\n\n        $adapter->{$getterName}('filename.txt');\n    }\n\n    public static function dpFailingMetadataGetters(): iterable\n    {\n        yield \"mimeType\" => [UnableToRetrieveMetadata::mimeType('filename.txt'), 'mimeType'];\n        yield \"lastModified\" => [UnableToRetrieveMetadata::lastModified('filename.txt'), 'lastModified'];\n        yield \"fileSize\" => [UnableToRetrieveMetadata::fileSize('filename.txt'), 'fileSize'];\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_check_for_file_existence(): void\n    {\n        $adapter = $this->adapter();\n        $exception = new ClientException(new SimpleMockedResponse());\n        static::$stubS3Client->throwExceptionWhenExecutingCommand('ObjectExists', $exception);\n\n        $this->expectException(UnableToCheckFileExistence::class);\n\n        $adapter->fileExists('something-that-does-exist.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function configuring_http_streaming_via_options(): void\n    {\n        $adapter = $this->useAdapter($this->createFilesystemAdapter());\n        $this->givenWeHaveAnExistingFile('path.txt');\n\n        $resource = $adapter->readStream('path.txt');\n        $metadata = stream_get_meta_data($resource);\n        fclose($resource);\n\n        $this->assertTrue($metadata['seekable']);\n    }\n\n    /**\n     * @test\n     */\n    public function write_with_s3_client(): void\n    {\n        $file = 'foo/bar.txt';\n        $prefix = 'all-files';\n        $bucket = 'foobar';\n        $contents = 'contents';\n\n        $s3Client = $this->getMockBuilder(S3Client::class)\n            ->disableOriginalConstructor()\n            ->onlyMethods(['putObject'])\n            ->getMock();\n        $s3Client->expects(self::once())\n            ->method('putObject')\n            ->with(self::callback(function (array $input) use ($file, $prefix, $bucket, $contents) {\n                if ($input['Key'] !== $prefix . '/' . $file) {\n                    return false;\n                }\n                if ($contents !== $input['Body']) {\n                    return false;\n                }\n                if ($input['Bucket'] !== $bucket) {\n                    return false;\n                }\n\n                return true;\n            }))->willReturn(ResultMockFactory::create(PutObjectOutput::class));\n\n        $filesystem = new AsyncAwsS3Adapter($s3Client, $bucket, $prefix);\n        $filesystem->write($file, $contents, new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function write_with_simple_s3_client(): void\n    {\n        $file = 'foo/bar.txt';\n        $prefix = 'all-files';\n        $bucket = 'foobar';\n        $contents = 'contents';\n\n        $s3Client = $this->getMockBuilder(SimpleS3Client::class)\n            ->disableOriginalConstructor()\n            ->onlyMethods(['upload', 'putObject'])\n            ->getMock();\n        $s3Client->expects(self::never())->method('putObject');\n        $s3Client->expects(self::once())\n            ->method('upload')\n            ->with($bucket, $prefix . '/' . $file, $contents);\n\n        $filesystem = new AsyncAwsS3Adapter($s3Client, $bucket, $prefix);\n        $filesystem->write($file, $contents, new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_write_a_file(): void\n    {\n        $adapter = $this->adapter();\n        static::$stubS3Client->throwExceptionWhenExecutingCommand('PutObject');\n        $this->expectException(UnableToWriteFile::class);\n\n        $adapter->write('foo/bar.txt', 'contents', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function moving_a_file_with_visibility(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be copied',\n                new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])\n            );\n            $adapter->move('source.txt', 'destination.txt', new Config([Config::OPTION_VISIBILITY => Visibility::PRIVATE]));\n            $this->assertFalse(\n                $adapter->fileExists('source.txt'),\n                'After moving a file should no longer exist in the original location.'\n            );\n            $this->assertTrue(\n                $adapter->fileExists('destination.txt'),\n                'After moving, a file should be present at the new location.'\n            );\n            $this->assertEquals(Visibility::PRIVATE, $adapter->visibility('destination.txt')->visibility());\n            $this->assertEquals('contents to be copied', $adapter->read('destination.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file_with_visibility(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be copied',\n                new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])\n            );\n\n            $adapter->copy('source.txt', 'destination.txt', new Config([Config::OPTION_VISIBILITY => Visibility::PRIVATE]));\n\n            $this->assertTrue($adapter->fileExists('source.txt'));\n            $this->assertTrue($adapter->fileExists('destination.txt'));\n            $this->assertEquals(Visibility::PRIVATE, $adapter->visibility('destination.txt')->visibility());\n            $this->assertEquals('contents to be copied', $adapter->read('destination.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file_with_non_ascii_characters(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'ıÇöü🤔.txt',\n                'contents to be copied',\n                new Config()\n            );\n\n            $adapter->copy('ıÇöü🤔.txt', 'ıÇöü🤔_copy.txt', new Config());\n\n            $this->assertTrue($adapter->fileExists('ıÇöü🤔.txt'));\n            $this->assertTrue($adapter->fileExists('ıÇöü🤔_copy.txt'));\n            $this->assertEquals('contents to be copied', $adapter->read('ıÇöü🤔_copy.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function top_level_directory_excluded_from_listing(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write('directory/file.txt', '', new Config());\n            $adapter->createDirectory('empty', new Config());\n            $adapter->createDirectory('nested/nested', new Config());\n            $listing1 = iterator_to_array($adapter->listContents('directory', true));\n            $listing2 = iterator_to_array($adapter->listContents('empty', true));\n            $listing3 = iterator_to_array($adapter->listContents('nested', true));\n\n            self::assertCount(1, $listing1);\n            self::assertCount(0, $listing2);\n            self::assertCount(1, $listing3);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_list_contents(): void\n    {\n        $adapter = $this->adapter();\n        static::$stubS3Client->throwExceptionWhenExecutingCommand('ListObjectsV2');\n\n        $this->expectException(UnableToListContents::class);\n\n        iterator_to_array($adapter->listContents('/path', false));\n    }\n\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        static::$stubS3Client = new S3ClientStub(static::s3Client(), self::awsConfig());\n        /** @var string $bucket */\n        $bucket = getenv('FLYSYSTEM_AWS_S3_BUCKET');\n        $prefix = getenv('FLYSYSTEM_AWS_S3_PREFIX') ?: static::$adapterPrefix;\n\n        return new AsyncAwsS3Adapter(static::$stubS3Client, $bucket, $prefix, null, null);\n    }\n}\n"
  },
  {
    "path": "src/AsyncAwsS3/LICENSE",
    "content": "Copyright (c) 2013-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/AsyncAwsS3/PortableVisibilityConverter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AsyncAwsS3;\n\nuse AsyncAws\\S3\\ValueObject\\Grant;\nuse League\\Flysystem\\Visibility;\n\nclass PortableVisibilityConverter implements VisibilityConverter\n{\n    private const PUBLIC_GRANTEE_URI = 'http://acs.amazonaws.com/groups/global/AllUsers';\n    private const PUBLIC_GRANTS_PERMISSION = 'READ';\n    private const PUBLIC_ACL = 'public-read';\n    private const PRIVATE_ACL = 'private';\n\n    /**\n     * @var string\n     */\n    private $defaultForDirectories;\n\n    public function __construct(string $defaultForDirectories = Visibility::PUBLIC)\n    {\n        $this->defaultForDirectories = $defaultForDirectories;\n    }\n\n    public function visibilityToAcl(string $visibility): string\n    {\n        if (Visibility::PUBLIC === $visibility) {\n            return self::PUBLIC_ACL;\n        }\n\n        return self::PRIVATE_ACL;\n    }\n\n    /**\n     * @param Grant[] $grants\n     */\n    public function aclToVisibility(array $grants): string\n    {\n        foreach ($grants as $grant) {\n            if (null === $grantee = $grant->getGrantee()) {\n                continue;\n            }\n            $granteeUri = $grantee->getURI();\n            $permission = $grant->getPermission();\n\n            if (self::PUBLIC_GRANTEE_URI === $granteeUri && self::PUBLIC_GRANTS_PERMISSION === $permission) {\n                return Visibility::PUBLIC;\n            }\n        }\n\n        return Visibility::PRIVATE;\n    }\n\n    public function defaultForDirectories(): string\n    {\n        return $this->defaultForDirectories;\n    }\n}\n"
  },
  {
    "path": "src/AsyncAwsS3/README.md",
    "content": "## Sub-split of Flysystem for AsyncAws S3.\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n\n```bash\ncomposer require league/flysystem-async-aws-s3\n```\n\nView the [documentation](https://flysystem.thephpleague.com/docs/adapter/async-aws-s3/)\n"
  },
  {
    "path": "src/AsyncAwsS3/S3ClientStub.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AsyncAwsS3;\n\nuse AsyncAws\\Core\\Exception\\Exception;\nuse AsyncAws\\Core\\Exception\\Http\\NetworkException;\nuse AsyncAws\\Core\\Result;\nuse AsyncAws\\S3\\Input\\CopyObjectRequest;\nuse AsyncAws\\S3\\Input\\DeleteObjectRequest;\nuse AsyncAws\\S3\\Input\\DeleteObjectsRequest;\nuse AsyncAws\\S3\\Input\\GetObjectAclRequest;\nuse AsyncAws\\S3\\Input\\GetObjectRequest;\nuse AsyncAws\\S3\\Input\\HeadObjectRequest;\nuse AsyncAws\\S3\\Input\\ListObjectsV2Request;\nuse AsyncAws\\S3\\Input\\PutObjectAclRequest;\nuse AsyncAws\\S3\\Input\\PutObjectRequest;\nuse AsyncAws\\S3\\Result\\CopyObjectOutput;\nuse AsyncAws\\S3\\Result\\DeleteObjectOutput;\nuse AsyncAws\\S3\\Result\\DeleteObjectsOutput;\nuse AsyncAws\\S3\\Result\\GetObjectAclOutput;\nuse AsyncAws\\S3\\Result\\GetObjectOutput;\nuse AsyncAws\\S3\\Result\\HeadObjectOutput;\nuse AsyncAws\\S3\\Result\\ListObjectsV2Output;\nuse AsyncAws\\S3\\Result\\ObjectExistsWaiter;\nuse AsyncAws\\S3\\Result\\PutObjectAclOutput;\nuse AsyncAws\\S3\\Result\\PutObjectOutput;\nuse AsyncAws\\S3\\S3Client;\nuse AsyncAws\\SimpleS3\\SimpleS3Client;\nuse DateTimeImmutable;\nuse Symfony\\Component\\HttpClient\\MockHttpClient;\n\n/**\n * @codeCoverageIgnore\n */\nclass S3ClientStub extends SimpleS3Client\n{\n    /**\n     * @var S3Client\n     */\n    private $actualClient;\n\n    /**\n     * @var Exception[]\n     */\n    private $stagedExceptions = [];\n\n    /**\n     * @var Result[]\n     */\n    private $stagedResult = [];\n\n    public function __construct(SimpleS3Client $client, $configuration = [])\n    {\n        $this->actualClient = $client;\n        parent::__construct($configuration, null, new MockHttpClient());\n    }\n\n    public function throwExceptionWhenExecutingCommand(string $commandName, ?Exception $exception = null): void\n    {\n        $this->stagedExceptions[$commandName] = $exception ?? new NetworkException();\n    }\n\n    public function stageResultForCommand(string $commandName, Result $result): void\n    {\n        $this->stagedResult[$commandName] = $result;\n    }\n\n    private function getStagedResult(string $name): ?Result\n    {\n        if (array_key_exists($name, $this->stagedExceptions)) {\n            $exception = $this->stagedExceptions[$name];\n            unset($this->stagedExceptions[$name]);\n\n            throw $exception;\n        }\n\n        if (array_key_exists($name, $this->stagedResult)) {\n            $result = $this->stagedResult[$name];\n            unset($this->stagedResult[$name]);\n\n            return $result;\n        }\n\n        return null;\n    }\n\n    /**\n     * @param array|CopyObjectRequest $input\n     */\n    public function copyObject($input): CopyObjectOutput\n    {\n        // @phpstan-ignore-next-line\n        return $this->getStagedResult('CopyObject') ?? $this->actualClient->copyObject($input);\n    }\n\n    /**\n     * @param array|DeleteObjectRequest $input\n     */\n    public function deleteObject($input): DeleteObjectOutput\n    {\n        // @phpstan-ignore-next-line\n        return $this->getStagedResult('DeleteObject') ?? $this->actualClient->deleteObject($input);\n    }\n\n    /**\n     * @param array|HeadObjectRequest $input\n     */\n    public function headObject($input): HeadObjectOutput\n    {\n        // @phpstan-ignore-next-line\n        return $this->getStagedResult('HeadObject') ?? $this->actualClient->headObject($input);\n    }\n\n    /**\n     * @param array|HeadObjectRequest $input\n     */\n    public function objectExists($input): ObjectExistsWaiter\n    {\n        // @phpstan-ignore-next-line\n        return $this->getStagedResult('ObjectExists') ?? $this->actualClient->objectExists($input);\n    }\n\n    /**\n     * @param array|ListObjectsV2Request $input\n     */\n    public function listObjectsV2($input): ListObjectsV2Output\n    {\n        // @phpstan-ignore-next-line\n        return $this->getStagedResult('ListObjectsV2') ?? $this->actualClient->listObjectsV2($input);\n    }\n\n    /**\n     * @param array|DeleteObjectsRequest $input\n     */\n    public function deleteObjects($input): DeleteObjectsOutput\n    {\n        // @phpstan-ignore-next-line\n        return $this->getStagedResult('DeleteObjects') ?? $this->actualClient->deleteObjects($input);\n    }\n\n    /**\n     * @param array|GetObjectAclRequest $input\n     */\n    public function getObjectAcl($input): GetObjectAclOutput\n    {\n        // @phpstan-ignore-next-line\n        return $this->getStagedResult('GetObjectAcl') ?? $this->actualClient->getObjectAcl($input);\n    }\n\n    /**\n     * @param array|PutObjectAclRequest $input\n     */\n    public function putObjectAcl($input): PutObjectAclOutput\n    {\n        // @phpstan-ignore-next-line\n        return $this->getStagedResult('PutObjectAcl') ?? $this->actualClient->putObjectAcl($input);\n    }\n\n    /**\n     * @param array|PutObjectRequest $input\n     */\n    public function putObject($input): PutObjectOutput\n    {\n        // @phpstan-ignore-next-line\n        return $this->getStagedResult('PutObject') ?? $this->actualClient->putObject($input);\n    }\n\n    /**\n     * @param array|GetObjectRequest $input\n     */\n    public function getObject($input): GetObjectOutput\n    {\n        // @phpstan-ignore-next-line\n        return $this->getStagedResult('GetObject') ?? $this->actualClient->getObject($input);\n    }\n\n    public function getUrl(string $bucket, string $key): string\n    {\n        return $this->actualClient->getUrl($bucket, $key);\n    }\n\n    public function getPresignedUrl(string $bucket, string $key, ?DateTimeImmutable $expires = null, ?string $versionId = null): string\n    {\n        return $this->actualClient->getPresignedUrl($bucket, $key, $expires);\n    }\n}\n"
  },
  {
    "path": "src/AsyncAwsS3/VisibilityConverter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AsyncAwsS3;\n\nuse AsyncAws\\S3\\ValueObject\\Grant;\n\ninterface VisibilityConverter\n{\n    public function visibilityToAcl(string $visibility): string;\n\n    /**\n     * @param Grant[] $grants\n     */\n    public function aclToVisibility(array $grants): string;\n\n    public function defaultForDirectories(): string;\n}\n"
  },
  {
    "path": "src/AsyncAwsS3/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-async-aws-s3\",\n    \"description\": \"AsyncAws S3 filesystem adapter for Flysystem.\",\n    \"keywords\": [\"async-aws\",\"aws\", \"s3\", \"flysystem\", \"filesystem\", \"storage\", \"file\", \"files\"],\n    \"type\": \"library\",\n    \"autoload\": {\n        \"psr-4\": {\n            \"League\\\\Flysystem\\\\AsyncAwsS3\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"league/flysystem\": \"^3.10.0\",\n        \"league/mime-type-detection\": \"^1.0.0\",\n        \"async-aws/s3\": \"^1.5 || ^2.0 || ^3.0\"\n    },\n    \"require-dev\": {\n        \"async-aws/simple-s3\": \"^2.1\"\n    },\n    \"conflict\": {\n        \"symfony/http-client\": \"<5.2\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ]\n}\n"
  },
  {
    "path": "src/AwsS3V3/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n**/*Test.php export-ignore\n**/*Stub.php export-ignore\nREADME.md export-ignore\n"
  },
  {
    "path": "src/AwsS3V3/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/AwsS3V3/AwsS3V3Adapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AwsS3V3;\n\nuse Aws\\Api\\DateTimeResult;\nuse Aws\\S3\\S3ClientInterface;\nuse DateTimeInterface;\nuse Generator;\nuse League\\Flysystem\\ChecksumAlgoIsNotSupported;\nuse League\\Flysystem\\ChecksumProvider;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\FilesystemOperationFailed;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\StorageAttributes;\nuse League\\Flysystem\\UnableToCheckDirectoryExistence;\nuse League\\Flysystem\\UnableToCheckFileExistence;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToGeneratePublicUrl;\nuse League\\Flysystem\\UnableToGenerateTemporaryUrl;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToProvideChecksum;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UrlGeneration\\PublicUrlGenerator;\nuse League\\Flysystem\\UrlGeneration\\TemporaryUrlGenerator;\nuse League\\Flysystem\\Visibility;\nuse League\\MimeTypeDetection\\FinfoMimeTypeDetector;\nuse League\\MimeTypeDetection\\MimeTypeDetector;\nuse Psr\\Http\\Message\\StreamInterface;\nuse Throwable;\nuse function trim;\n\nclass AwsS3V3Adapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider, TemporaryUrlGenerator\n{\n    /**\n     * @var string[]\n     */\n    public const AVAILABLE_OPTIONS = [\n        'ACL',\n        'CacheControl',\n        'ContentDisposition',\n        'ContentEncoding',\n        'ContentLength',\n        'ContentType',\n        'Expires',\n        'GrantFullControl',\n        'GrantRead',\n        'GrantReadACP',\n        'GrantWriteACP',\n        'Metadata',\n        'MetadataDirective',\n        'RequestPayer',\n        'SSECustomerAlgorithm',\n        'SSECustomerKey',\n        'SSECustomerKeyMD5',\n        'SSEKMSKeyId',\n        'ServerSideEncryption',\n        'StorageClass',\n        'Tagging',\n        'WebsiteRedirectLocation',\n        'ChecksumAlgorithm',\n        'CopySourceSSECustomerAlgorithm',\n        'CopySourceSSECustomerKey',\n        'CopySourceSSECustomerKeyMD5',\n    ];\n    /**\n     * @var string[]\n     */\n    public const MUP_AVAILABLE_OPTIONS = [\n        'add_content_md5',\n        'before_upload',\n        'concurrency',\n        'mup_threshold',\n        'params',\n        'part_size',\n    ];\n\n    /**\n     * @var string[]\n     */\n    private const EXTRA_METADATA_FIELDS = [\n        'Metadata',\n        'StorageClass',\n        'ETag',\n        'VersionId',\n    ];\n\n    private PathPrefixer $prefixer;\n    private VisibilityConverter $visibility;\n    private MimeTypeDetector $mimeTypeDetector;\n\n    public function __construct(\n        private S3ClientInterface $client,\n        private string $bucket,\n        string $prefix = '',\n        ?VisibilityConverter $visibility = null,\n        ?MimeTypeDetector $mimeTypeDetector = null,\n        private array $options = [],\n        private bool $streamReads = true,\n        private array $forwardedOptions = self::AVAILABLE_OPTIONS,\n        private array $metadataFields = self::EXTRA_METADATA_FIELDS,\n        private array $multipartUploadOptions = self::MUP_AVAILABLE_OPTIONS,\n    ) {\n        $this->prefixer = new PathPrefixer($prefix);\n        $this->visibility = $visibility ?? new PortableVisibilityConverter();\n        $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();\n    }\n\n    public function fileExists(string $path): bool\n    {\n        try {\n            return $this->client->doesObjectExistV2($this->bucket, $this->prefixer->prefixPath($path), false, $this->options);\n        } catch (Throwable $exception) {\n            throw UnableToCheckFileExistence::forLocation($path, $exception);\n        }\n    }\n\n    public function directoryExists(string $path): bool\n    {\n        try {\n            $prefix = $this->prefixer->prefixDirectoryPath($path);\n            $options = ['Bucket' => $this->bucket, 'Prefix' => $prefix, 'MaxKeys' => 1, 'Delimiter' => '/'];\n            $command = $this->client->getCommand('ListObjectsV2', $options);\n            $result = $this->client->execute($command);\n\n            return $result->hasKey('Contents') || $result->hasKey('CommonPrefixes');\n        } catch (Throwable $exception) {\n            throw UnableToCheckDirectoryExistence::forLocation($path, $exception);\n        }\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        $this->upload($path, $contents, $config);\n    }\n\n    /**\n     * @param string          $path\n     * @param string|resource $body\n     * @param Config          $config\n     */\n    private function upload(string $path, $body, Config $config): void\n    {\n        $key = $this->prefixer->prefixPath($path);\n        $options = $this->createOptionsFromConfig($config);\n        $acl = $options['params']['ACL'] ?? $this->determineAcl($config);\n        $shouldDetermineMimetype = ! array_key_exists('ContentType', $options['params']);\n\n        if ($shouldDetermineMimetype && $mimeType = $this->mimeTypeDetector->detectMimeType($key, $body)) {\n            $options['params']['ContentType'] = $mimeType;\n        }\n\n        try {\n            $this->client->upload($this->bucket, $key, $body, $acl, $options);\n        } catch (Throwable $exception) {\n            throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    private function determineAcl(Config $config): string\n    {\n        $visibility = (string) $config->get(Config::OPTION_VISIBILITY, Visibility::PRIVATE);\n\n        return $this->visibility->visibilityToAcl($visibility);\n    }\n\n    private function createOptionsFromConfig(Config $config): array\n    {\n        $config = $config->withDefaults($this->options);\n        $options = ['params' => []];\n\n        if ($mimetype = $config->get('mimetype')) {\n            $options['params']['ContentType'] = $mimetype;\n        }\n\n        foreach ($this->forwardedOptions as $option) {\n            $value = $config->get($option, '__NOT_SET__');\n\n            if ($value !== '__NOT_SET__') {\n                $options['params'][$option] = $value;\n            }\n        }\n\n        foreach ($this->multipartUploadOptions as $option) {\n            $value = $config->get($option, '__NOT_SET__');\n\n            if ($value !== '__NOT_SET__') {\n                $options[$option] = $value;\n            }\n        }\n\n        return $options;\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        $this->upload($path, $contents, $config);\n    }\n\n    public function read(string $path): string\n    {\n        $body = $this->readObject($path, false);\n\n        return (string) $body->getContents();\n    }\n\n    public function readStream(string $path)\n    {\n        /** @var resource $resource */\n        $resource = $this->readObject($path, true)->detach();\n\n        return $resource;\n    }\n\n    public function delete(string $path): void\n    {\n        $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];\n        $command = $this->client->getCommand('DeleteObject', $arguments);\n\n        try {\n            $this->client->execute($command);\n        } catch (Throwable $exception) {\n            throw UnableToDeleteFile::atLocation($path, '', $exception);\n        }\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        $prefix = $this->prefixer->prefixPath($path);\n        $prefix = ltrim(rtrim($prefix, '/') . '/', '/');\n\n        try {\n            $this->client->deleteMatchingObjects($this->bucket, $prefix);\n        } catch (Throwable $exception) {\n            throw UnableToDeleteDirectory::atLocation($path, '', $exception);\n        }\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        $defaultVisibility = $config->get(Config::OPTION_DIRECTORY_VISIBILITY, $this->visibility->defaultForDirectories());\n        $config = $config->withDefaults([Config::OPTION_VISIBILITY => $defaultVisibility]);\n        $this->upload(rtrim($path, '/') . '/', '', $config);\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        $arguments = [\n            'Bucket' => $this->bucket,\n            'Key' => $this->prefixer->prefixPath($path),\n            'ACL' => $this->visibility->visibilityToAcl($visibility),\n        ];\n        $command = $this->client->getCommand('PutObjectAcl', $arguments);\n\n        try {\n            $this->client->execute($command);\n        } catch (Throwable $exception) {\n            throw UnableToSetVisibility::atLocation($path, '', $exception);\n        }\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];\n        $command = $this->client->getCommand('GetObjectAcl', $arguments);\n\n        try {\n            $result = $this->client->execute($command);\n        } catch (Throwable $exception) {\n            throw UnableToRetrieveMetadata::visibility($path, '', $exception);\n        }\n\n        $visibility = $this->visibility->aclToVisibility((array) $result->get('Grants'));\n\n        return new FileAttributes($path, null, $visibility);\n    }\n\n    private function fetchFileMetadata(string $path, string $type): FileAttributes\n    {\n        $options = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];\n        $command = $this->client->getCommand('HeadObject', $options + $this->options);\n\n        try {\n            $result = $this->client->execute($command);\n        } catch (Throwable $exception) {\n            throw UnableToRetrieveMetadata::create($path, $type, '', $exception);\n        }\n\n        $attributes = $this->mapS3ObjectMetadata($result->toArray(), $path);\n\n        if ( ! $attributes instanceof FileAttributes) {\n            throw UnableToRetrieveMetadata::create($path, $type, '');\n        }\n\n        return $attributes;\n    }\n\n    private function mapS3ObjectMetadata(array $metadata, string $path): StorageAttributes\n    {\n        if (substr($path, -1) === '/') {\n            return new DirectoryAttributes(rtrim($path, '/'));\n        }\n\n        $mimetype = $metadata['ContentType'] ?? null;\n        $fileSize = $metadata['ContentLength'] ?? $metadata['Size'] ?? null;\n        $fileSize = $fileSize === null ? null : (int) $fileSize;\n        $dateTime = $metadata['LastModified'] ?? null;\n        $lastModified = $dateTime instanceof DateTimeResult ? $dateTime->getTimeStamp() : null;\n\n        return new FileAttributes(\n            $path,\n            $fileSize,\n            null,\n            $lastModified,\n            $mimetype,\n            $this->extractExtraMetadata($metadata)\n        );\n    }\n\n    private function extractExtraMetadata(array $metadata): array\n    {\n        $extracted = [];\n\n        foreach ($this->metadataFields as $field) {\n            if (isset($metadata[$field]) && $metadata[$field] !== '') {\n                $extracted[$field] = $metadata[$field];\n            }\n        }\n\n        return $extracted;\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        $attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_MIME_TYPE);\n\n        if ($attributes->mimeType() === null) {\n            throw UnableToRetrieveMetadata::mimeType($path);\n        }\n\n        return $attributes;\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        $attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_LAST_MODIFIED);\n\n        if ($attributes->lastModified() === null) {\n            throw UnableToRetrieveMetadata::lastModified($path);\n        }\n\n        return $attributes;\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        $attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_FILE_SIZE);\n\n        if ($attributes->fileSize() === null) {\n            throw UnableToRetrieveMetadata::fileSize($path);\n        }\n\n        return $attributes;\n    }\n\n    public function listContents(string $path, bool $deep): iterable\n    {\n        $prefix = trim($this->prefixer->prefixPath($path), '/');\n        $prefix = $prefix === '' ? '' : $prefix . '/';\n        $options = ['Bucket' => $this->bucket, 'Prefix' => $prefix];\n\n        if ($deep === false) {\n            $options['Delimiter'] = '/';\n        }\n\n        $listing = $this->retrievePaginatedListing($options);\n\n        foreach ($listing as $item) {\n            $key = $item['Key'] ?? $item['Prefix'];\n\n            if ($key === $prefix) {\n                continue;\n            }\n\n            yield $this->mapS3ObjectMetadata($item, $this->prefixer->stripPrefix($key));\n        }\n    }\n\n    private function retrievePaginatedListing(array $options): Generator\n    {\n        $resultPaginator = $this->client->getPaginator('ListObjectsV2', $options + $this->options);\n\n        foreach ($resultPaginator as $result) {\n            yield from ($result->get('CommonPrefixes') ?? []);\n            yield from ($result->get('Contents') ?? []);\n        }\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        if ($source === $destination) {\n            return;\n        }\n\n        try {\n            $this->copy($source, $destination, $config);\n            $this->delete($source);\n        } catch (FilesystemOperationFailed $exception) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        if ($source === $destination) {\n            return;\n        }\n\n        try {\n            $visibility = $config->get(Config::OPTION_VISIBILITY);\n\n            if ($visibility === null && $config->get(Config::OPTION_RETAIN_VISIBILITY, true)) {\n                $visibility = $this->visibility($source)->visibility();\n            }\n        } catch (Throwable $exception) {\n            throw UnableToCopyFile::fromLocationTo(\n                $source,\n                $destination,\n                $exception\n            );\n        }\n\n        $options = $this->createOptionsFromConfig($config);\n        $options['MetadataDirective'] = $config->get('MetadataDirective', 'COPY');\n\n        try {\n            $this->client->copy(\n                $this->bucket,\n                $this->prefixer->prefixPath($source),\n                $this->bucket,\n                $this->prefixer->prefixPath($destination),\n                $this->visibility->visibilityToAcl($visibility ?: 'private'),\n                $options,\n            );\n        } catch (Throwable $exception) {\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    private function readObject(string $path, bool $wantsStream): StreamInterface\n    {\n        $options = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];\n\n        if ($wantsStream && $this->streamReads && ! isset($this->options['@http']['stream'])) {\n            $options['@http']['stream'] = true;\n        }\n\n        $command = $this->client->getCommand('GetObject', $options + $this->options);\n\n        try {\n            return $this->client->execute($command)->get('Body');\n        } catch (Throwable $exception) {\n            throw UnableToReadFile::fromLocation($path, '', $exception);\n        }\n    }\n\n    public function publicUrl(string $path, Config $config): string\n    {\n        $location = $this->prefixer->prefixPath($path);\n\n        try {\n            return $this->client->getObjectUrl($this->bucket, $location);\n        } catch (Throwable $exception) {\n            throw UnableToGeneratePublicUrl::dueToError($path, $exception);\n        }\n    }\n\n    public function checksum(string $path, Config $config): string\n    {\n        $algo = $config->get('checksum_algo', 'etag');\n\n        if ($algo !== 'etag') {\n            throw new ChecksumAlgoIsNotSupported();\n        }\n\n        try {\n            $metadata = $this->fetchFileMetadata($path, 'checksum')->extraMetadata();\n        } catch (UnableToRetrieveMetadata $exception) {\n            throw new UnableToProvideChecksum($exception->reason(), $path, $exception);\n        }\n\n        if ( ! isset($metadata['ETag'])) {\n            throw new UnableToProvideChecksum('ETag header not available.', $path);\n        }\n\n        return trim($metadata['ETag'], '\"');\n    }\n\n    public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string\n    {\n        try {\n            $options = $config->get('get_object_options', []);\n            $command = $this->client->getCommand('GetObject', [\n                    'Bucket' => $this->bucket,\n                    'Key' => $this->prefixer->prefixPath($path),\n                ] + $options);\n\n            $presignedRequestOptions = $config->get('presigned_request_options', []);\n            $request = $this->client->createPresignedRequest($command, $expiresAt, $presignedRequestOptions);\n\n            return (string) $request->getUri();\n        } catch (Throwable $exception) {\n            throw UnableToGenerateTemporaryUrl::dueToError($path, $exception);\n        }\n    }\n}\n"
  },
  {
    "path": "src/AwsS3V3/AwsS3V3AdapterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AwsS3V3;\n\nuse Aws\\Result;\nuse Aws\\S3\\S3Client;\nuse Aws\\S3\\S3ClientInterface;\nuse Exception;\nuse Generator;\nuse League\\Flysystem\\AdapterTestUtilities\\FilesystemAdapterTestCase;\nuse League\\Flysystem\\ChecksumAlgoIsNotSupported;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\StorageAttributes;\nuse League\\Flysystem\\UnableToCheckFileExistence;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\Visibility;\nuse RuntimeException;\n\nuse function getenv;\nuse function iterator_to_array;\n\n/**\n * @group aws\n */\nclass AwsS3V3AdapterTest extends FilesystemAdapterTestCase\n{\n    /**\n     * @var bool\n     */\n    private $shouldCleanUp = false;\n\n    /**\n     * @var string\n     */\n    private static $adapterPrefix = 'test-prefix';\n\n    /**\n     * @var S3ClientInterface|null\n     */\n    private static $s3Client;\n\n    /**\n     * @var S3ClientStub\n     */\n    private static $stubS3Client;\n\n    public static function setUpBeforeClass(): void\n    {\n        static::$adapterPrefix = getenv('FLYSYSTEM_AWS_S3_PREFIX') ?: 'ci/' . bin2hex(random_bytes(10));\n    }\n\n    protected function tearDown(): void\n    {\n        if ( ! $this->shouldCleanUp) {\n            return;\n        }\n\n        $adapter = $this->adapter();\n        $adapter->deleteDirectory('/');\n        /** @var StorageAttributes[] $listing */\n        $listing = $adapter->listContents('', false);\n\n        foreach ($listing as $item) {\n            if ($item->isFile()) {\n                $adapter->delete($item->path());\n            } else {\n                $adapter->deleteDirectory($item->path());\n            }\n        }\n\n        self::$adapter = null;\n    }\n\n    protected function setUp(): void\n    {\n        if (PHP_VERSION_ID < 80100) {\n            $this->markTestSkipped('AWS does not support this anymore.');\n        }\n\n        parent::setUp();\n    }\n\n    private static function s3Client(): S3ClientInterface\n    {\n        if (static::$s3Client instanceof S3ClientInterface) {\n            return static::$s3Client;\n        }\n\n        $key = getenv('FLYSYSTEM_AWS_S3_KEY');\n        $secret = getenv('FLYSYSTEM_AWS_S3_SECRET');\n        $bucket = getenv('FLYSYSTEM_AWS_S3_BUCKET');\n        $region = getenv('FLYSYSTEM_AWS_S3_REGION') ?: 'eu-central-1';\n\n        if ( ! $key || ! $secret || ! $bucket) {\n            self::markTestSkipped('No AWS credentials present for testing.');\n        }\n\n        $options = ['version' => 'latest', 'credentials' => compact('key', 'secret'), 'region' => $region];\n\n        return static::$s3Client = new S3Client($options);\n    }\n\n    /**\n     * @test\n     */\n    public function writing_with_a_specific_mime_type(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('some/path.txt', 'contents', new Config(['ContentType' => 'text/plain+special']));\n        $mimeType = $adapter->mimeType('some/path.txt')->mimeType();\n        $this->assertEquals('text/plain+special', $mimeType);\n    }\n\n    /**\n     * @test\n     */\n    public function writing_a_file_with_explicit_mime_type(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('some/path.txt', 'contents', new Config(['mimetype' => 'text/plain+special']));\n        $mimeType = $adapter->mimeType('some/path.txt')->mimeType();\n        $this->assertEquals('text/plain+special', $mimeType);\n    }\n\n    /**\n     * @test\n     *\n     * @see https://github.com/thephpleague/flysystem-aws-s3-v3/issues/291\n     */\n    public function issue_291(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->createDirectory('directory', new Config());\n        $listing = iterator_to_array($adapter->listContents('directory', true));\n\n        self::assertCount(0, $listing);\n    }\n\n    /**\n     * @test\n     */\n    public function listing_contents_recursive(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('something/0/here.txt', 'contents', new Config());\n        $adapter->write('something/1/also/here.txt', 'contents', new Config());\n\n        $contents = iterator_to_array($adapter->listContents('', true));\n\n        $this->assertCount(2, $contents);\n        $this->assertContainsOnlyInstancesOf(FileAttributes::class, $contents);\n        /** @var FileAttributes $file */\n        $file = $contents[0];\n        $this->assertEquals('something/0/here.txt', $file->path());\n        /** @var FileAttributes $file */\n        $file = $contents[1];\n        $this->assertEquals('something/1/also/here.txt', $file->path());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_delete_while_moving(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('source.txt', 'contents to be copied', new Config());\n        static::$stubS3Client->failOnNextCopy();\n\n        $this->expectException(UnableToMoveFile::class);\n\n        $adapter->move('source.txt', 'destination.txt', new Config());\n    }\n\n    /**\n     * @test\n     *\n     * @see https://github.com/thephpleague/flysystem-aws-s3-v3/issues/287\n     */\n    public function issue_287(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('KmFVvKqo/QLMExy2U/620ff60c8a154.pdf', 'pdf content', new Config());\n\n        self::assertTrue($adapter->directoryExists('KmFVvKqo'));\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_write_a_file(): void\n    {\n        $adapter = $this->adapter();\n        static::$stubS3Client->throwDuringUpload(new RuntimeException('Oh no'));\n\n        $this->expectException(UnableToWriteFile::class);\n\n        $adapter->write('path.txt', 'contents', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_delete_a_file(): void\n    {\n        $adapter = $this->adapter();\n        static::$stubS3Client->throwExceptionWhenExecutingCommand('DeleteObject');\n\n        $this->expectException(UnableToDeleteFile::class);\n\n        $adapter->delete('path.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_unknown_mime_type_of_a_file(): void\n    {\n        $this->adapter();\n        $result = new Result([\n            'Key' => static::$adapterPrefix . '/unknown-mime-type.md5',\n        ]);\n        static::$stubS3Client->stageResultForCommand('HeadObject', $result);\n\n        parent::fetching_unknown_mime_type_of_a_file();\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider dpFailingMetadataGetters\n     */\n    public function failing_to_retrieve_metadata(Exception $exception, string $getterName): void\n    {\n        $adapter = $this->adapter();\n        $result = new Result([\n             'Key' => static::$adapterPrefix . '/filename.txt',\n        ]);\n        static::$stubS3Client->stageResultForCommand('HeadObject', $result);\n\n        $this->expectExceptionObject($exception);\n\n        $adapter->{$getterName}('filename.txt');\n    }\n\n    public static function dpFailingMetadataGetters(): iterable\n    {\n        yield \"mimeType\" => [UnableToRetrieveMetadata::mimeType('filename.txt'), 'mimeType'];\n        yield \"lastModified\" => [UnableToRetrieveMetadata::lastModified('filename.txt'), 'lastModified'];\n        yield \"fileSize\" => [UnableToRetrieveMetadata::fileSize('filename.txt'), 'fileSize'];\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_check_for_file_existence(): void\n    {\n        $adapter = $this->adapter();\n\n        static::$stubS3Client->throw500ExceptionWhenExecutingCommand('HeadObject');\n\n        $this->expectException(UnableToCheckFileExistence::class);\n\n        $adapter->fileExists('something-that-does-exist.txt');\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider casesWhereHttpStreamingInfluencesSeekability\n     */\n    public function streaming_reads_are_not_seekable_and_non_streaming_are(bool $streaming, bool $seekable): void\n    {\n        if (getenv('COMPOSER_OPTS') === '--prefer-lowest') {\n            $this->markTestSkipped('The SDK does not support streaming in low versions.');\n        }\n\n        $adapter = $this->useAdapter($this->createFilesystemAdapter($streaming));\n        $this->givenWeHaveAnExistingFile('path.txt');\n\n        $resource = $adapter->readStream('path.txt');\n        $metadata = stream_get_meta_data($resource);\n        fclose($resource);\n\n        $this->assertEquals($seekable, $metadata['seekable']);\n    }\n\n    public static function casesWhereHttpStreamingInfluencesSeekability(): Generator\n    {\n        yield \"not streaming reads have seekable stream\" => [false, true];\n        yield \"streaming reads have non-seekable stream\" => [true, false];\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider casesWhereHttpStreamingInfluencesSeekability\n     */\n    public function configuring_http_streaming_via_options(bool $streaming): void\n    {\n        $adapter = $this->useAdapter($this->createFilesystemAdapter($streaming, ['@http' => ['stream' => false]]));\n        $this->givenWeHaveAnExistingFile('path.txt');\n\n        $resource = $adapter->readStream('path.txt');\n        $metadata = stream_get_meta_data($resource);\n        fclose($resource);\n\n        $this->assertTrue($metadata['seekable']);\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider casesWhereHttpStreamingInfluencesSeekability\n     */\n    public function use_globally_configured_options(bool $streaming): void\n    {\n        $adapter = $this->useAdapter($this->createFilesystemAdapter($streaming, ['ContentType' => 'text/plain+special']));\n        $this->givenWeHaveAnExistingFile('path.txt');\n\n        $mimeType = $adapter->mimeType('path.txt')->mimeType();\n        $this->assertSame('text/plain+special', $mimeType);\n    }\n\n    /**\n     * @test\n     */\n    public function moving_with_updated_metadata(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('source.txt', 'contents to be moved', new Config(['ContentType' => 'text/plain']));\n        $mimeTypeSource = $adapter->mimeType('source.txt')->mimeType();\n        $this->assertSame('text/plain', $mimeTypeSource);\n\n        $adapter->move('source.txt', 'destination.txt', new Config(\n            ['ContentType' => 'text/plain+special', 'MetadataDirective' => 'REPLACE']\n        ));\n        $mimeTypeDestination = $adapter->mimeType('destination.txt')->mimeType();\n        $this->assertSame('text/plain+special', $mimeTypeDestination);\n    }\n\n    /**\n     * @test\n     */\n    public function moving_without_updated_metadata(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('source.txt', 'contents to be moved', new Config(['ContentType' => 'text/plain']));\n        $mimeTypeSource = $adapter->mimeType('source.txt')->mimeType();\n        $this->assertSame('text/plain', $mimeTypeSource);\n\n        $adapter->move('source.txt', 'destination.txt', new Config(\n            ['ContentType' => 'text/plain+special']\n        ));\n        $mimeTypeDestination = $adapter->mimeType('destination.txt')->mimeType();\n        $this->assertSame('text/plain', $mimeTypeDestination);\n    }\n\n    /**\n     * @test\n     */\n    public function copying_with_updated_metadata(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('source.txt', 'contents to be moved', new Config(['ContentType' => 'text/plain']));\n        $mimeTypeSource = $adapter->mimeType('source.txt')->mimeType();\n        $this->assertSame('text/plain', $mimeTypeSource);\n\n        $adapter->copy('source.txt', 'destination.txt', new Config(\n            ['ContentType' => 'text/plain+special', 'MetadataDirective' => 'REPLACE']\n        ));\n        $mimeTypeDestination = $adapter->mimeType('destination.txt')->mimeType();\n        $this->assertSame('text/plain+special', $mimeTypeDestination);\n    }\n\n    /**\n     * @test\n     */\n    public function setting_acl_via_options(): void\n    {\n        $adapter = $this->adapter();\n        $prefixer = new PathPrefixer(static::$adapterPrefix);\n        $prefixedPath = $prefixer->prefixPath('path.txt');\n\n        $adapter->write('path.txt', 'contents', new Config(['ACL' => 'bucket-owner-full-control']));\n        $arguments = ['Bucket' => getenv('FLYSYSTEM_AWS_S3_BUCKET'), 'Key' => $prefixedPath];\n        $command = static::$s3Client->getCommand('GetObjectAcl', $arguments);\n        $response = static::$s3Client->execute($command)->toArray();\n        $permission = $response['Grants'][0]['Permission'];\n\n        self::assertEquals('FULL_CONTROL', $permission);\n    }\n\n    /**\n     * @test\n     */\n    public function moving_a_file_with_visibility(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be copied',\n                new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])\n            );\n            $adapter->move('source.txt', 'destination.txt', new Config([Config::OPTION_VISIBILITY => Visibility::PRIVATE]));\n            $this->assertFalse(\n                $adapter->fileExists('source.txt'),\n                'After moving a file should no longer exist in the original location.'\n            );\n            $this->assertTrue(\n                $adapter->fileExists('destination.txt'),\n                'After moving, a file should be present at the new location.'\n            );\n            $this->assertEquals(Visibility::PRIVATE, $adapter->visibility('destination.txt')->visibility());\n            $this->assertEquals('contents to be copied', $adapter->read('destination.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function specifying_a_custom_checksum_algo_is_not_supported(): void\n    {\n        /** @var AwsS3V3Adapter $adapter */\n        $adapter = $this->adapter();\n\n        $this->expectException(ChecksumAlgoIsNotSupported::class);\n\n        $adapter->checksum('something', new Config(['checksum_algo' => 'md5']));\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file_with_visibility(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be copied',\n                new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])\n            );\n\n            $adapter->copy('source.txt', 'destination.txt', new Config([Config::OPTION_VISIBILITY => Visibility::PRIVATE]));\n\n            $this->assertTrue($adapter->fileExists('source.txt'));\n            $this->assertTrue($adapter->fileExists('destination.txt'));\n            $this->assertEquals(Visibility::PRIVATE, $adapter->visibility('destination.txt')->visibility());\n            $this->assertEquals('contents to be copied', $adapter->read('destination.txt'));\n        });\n    }\n\n    protected static function createFilesystemAdapter(bool $streaming = true, array $options = []): FilesystemAdapter\n    {\n        static::$stubS3Client = new S3ClientStub(static::s3Client());\n        /** @var string $bucket */\n        $bucket = getenv('FLYSYSTEM_AWS_S3_BUCKET');\n        $prefix = static::$adapterPrefix;\n\n        return new AwsS3V3Adapter(static::$stubS3Client, $bucket, $prefix, null, null, $options, $streaming);\n    }\n}\n"
  },
  {
    "path": "src/AwsS3V3/LICENSE",
    "content": "Copyright (c) 2013-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/AwsS3V3/PortableVisibilityConverter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AwsS3V3;\n\nuse League\\Flysystem\\Visibility;\n\nclass PortableVisibilityConverter implements VisibilityConverter\n{\n    private const PUBLIC_GRANTEE_URI = 'http://acs.amazonaws.com/groups/global/AllUsers';\n    private const PUBLIC_GRANTS_PERMISSION = 'READ';\n    private const PUBLIC_ACL = 'public-read';\n    private const PRIVATE_ACL = 'private';\n\n    public function __construct(private string $defaultForDirectories = Visibility::PUBLIC)\n    {\n    }\n\n    public function visibilityToAcl(string $visibility): string\n    {\n        if ($visibility === Visibility::PUBLIC) {\n            return self::PUBLIC_ACL;\n        }\n\n        return self::PRIVATE_ACL;\n    }\n\n    public function aclToVisibility(array $grants): string\n    {\n        foreach ($grants as $grant) {\n            $granteeUri = $grant['Grantee']['URI'] ?? null;\n            $permission = $grant['Permission'] ?? null;\n\n            if ($granteeUri === self::PUBLIC_GRANTEE_URI && $permission === self::PUBLIC_GRANTS_PERMISSION) {\n                return Visibility::PUBLIC;\n            }\n        }\n\n        return Visibility::PRIVATE;\n    }\n\n    public function defaultForDirectories(): string\n    {\n        return $this->defaultForDirectories;\n    }\n}\n"
  },
  {
    "path": "src/AwsS3V3/README.md",
    "content": "## Sub-split of Flysystem for AWS S3.\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n\n```bash\ncomposer require league/flysystem-aws-s3-v3\n```\n\nView the [documentation](https://flysystem.thephpleague.com/docs/adapter/aws-s3-v3/).\n"
  },
  {
    "path": "src/AwsS3V3/S3ClientStub.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AwsS3V3;\n\nuse Aws\\Command;\nuse Aws\\CommandInterface;\nuse Aws\\ResultInterface;\nuse Aws\\S3\\Exception\\S3Exception;\nuse Aws\\S3\\S3ClientInterface;\nuse Aws\\S3\\S3ClientTrait;\nuse GuzzleHttp\\Psr7\\Response;\n\nuse Throwable;\n\nuse function GuzzleHttp\\Promise\\promise_for;\n\n/**\n * @codeCoverageIgnore\n */\nclass S3ClientStub implements S3ClientInterface\n{\n    use S3ClientTrait;\n\n    /**\n     * @var S3ClientInterface\n     */\n    private $actualClient;\n\n    /**\n     * @var S3Exception[]\n     */\n    private $stagedExceptions = [];\n\n    /**\n     * @var ResultInterface[]\n     */\n    private $stagedResult = [];\n\n    /**\n     * @var Throwable|null\n     */\n    private $exceptionForUpload = null;\n\n    public function __construct(S3ClientInterface $client)\n    {\n        return $this->actualClient = $client;\n    }\n\n    public function throwDuringUpload(Throwable $throwable): void\n    {\n        $this->exceptionForUpload = $throwable;\n    }\n\n    public function upload($bucket, $key, $body, $acl = 'private', array $options = [])\n    {\n        if ($this->exceptionForUpload instanceof Throwable) {\n            $throwable = $this->exceptionForUpload;\n            $this->exceptionForUpload = null;\n            throw $throwable;\n        }\n\n        return $this->actualClient->upload($bucket, $key, $body, $acl, $options);\n    }\n\n    public function failOnNextCopy(): void\n    {\n        $this->throwExceptionWhenExecutingCommand('CopyObject');\n    }\n\n    public function throwExceptionWhenExecutingCommand(string $commandName, ?S3Exception $exception = null): void\n    {\n        $this->stagedExceptions[$commandName] = $exception ?? new S3Exception($commandName, new Command($commandName));\n    }\n\n    public function throw500ExceptionWhenExecutingCommand(string $commandName): void\n    {\n        $response = new Response(500);\n        $exception = new S3Exception($commandName, new Command($commandName), compact('response'));\n\n        $this->throwExceptionWhenExecutingCommand($commandName, $exception);\n    }\n\n    public function stageResultForCommand(string $commandName, ResultInterface $result): void\n    {\n        $this->stagedResult[$commandName] = $result;\n    }\n\n    public function execute(CommandInterface $command)\n    {\n        return $this->executeAsync($command)->wait();\n    }\n\n    public function getCommand($name, array $args = [])\n    {\n        return $this->actualClient->getCommand($name, $args);\n    }\n\n    public function getHandlerList()\n    {\n        return $this->actualClient->getHandlerList();\n    }\n\n    public function getIterator($name, array $args = [])\n    {\n        return $this->actualClient->getIterator($name, $args);\n    }\n\n    public function __call($name, array $arguments)\n    {\n        return $this->actualClient->__call($name, $arguments);\n    }\n\n    public function executeAsync(CommandInterface $command)\n    {\n        $name = $command->getName();\n\n        if (array_key_exists($name, $this->stagedExceptions)) {\n            $exception = $this->stagedExceptions[$name];\n            unset($this->stagedExceptions[$name]);\n            throw $exception;\n        }\n\n        if (array_key_exists($name, $this->stagedResult)) {\n            $result = $this->stagedResult[$name];\n            unset($this->stagedResult[$name]);\n\n            return promise_for($result);\n        }\n\n        return $this->actualClient->executeAsync($command);\n    }\n\n    public function getCredentials()\n    {\n        return $this->actualClient->getCredentials();\n    }\n\n    public function getRegion()\n    {\n        return $this->actualClient->getRegion();\n    }\n\n    public function getEndpoint()\n    {\n        return $this->actualClient->getEndpoint();\n    }\n\n    public function getApi()\n    {\n        return $this->actualClient->getApi();\n    }\n\n    public function getConfig($option = null)\n    {\n        return $this->actualClient->getConfig($option);\n    }\n\n    public function getPaginator($name, array $args = [])\n    {\n        return $this->actualClient->getPaginator($name, $args);\n    }\n\n    public function waitUntil($name, array $args = [])\n    {\n        $this->actualClient->waitUntil($name, $args);\n    }\n\n    public function getWaiter($name, array $args = [])\n    {\n        return $this->actualClient->getWaiter($name, $args);\n    }\n\n    public function createPresignedRequest(CommandInterface $command, $expires, array $options = [])\n    {\n        return $this->actualClient->createPresignedRequest($command, $expires, $options);\n    }\n\n    public function getObjectUrl($bucket, $key)\n    {\n        return $this->actualClient->getObjectUrl($bucket, $key);\n    }\n}\n"
  },
  {
    "path": "src/AwsS3V3/VisibilityConverter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AwsS3V3;\n\ninterface VisibilityConverter\n{\n    public function visibilityToAcl(string $visibility): string;\n    public function aclToVisibility(array $grants): string;\n    public function defaultForDirectories(): string;\n}\n"
  },
  {
    "path": "src/AwsS3V3/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-aws-s3-v3\",\n    \"description\": \"AWS S3 filesystem adapter for Flysystem.\",\n    \"keywords\": [\"aws\", \"s3\", \"flysystem\", \"filesystem\", \"storage\", \"file\", \"files\"],\n    \"type\": \"library\",\n    \"autoload\": {\n        \"psr-4\": {\n            \"League\\\\Flysystem\\\\AwsS3V3\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"league/flysystem\": \"^3.10.0\",\n        \"league/mime-type-detection\": \"^1.0.0\",\n        \"aws/aws-sdk-php\": \"^3.295.10\"\n    },\n    \"conflict\": {\n        \"guzzlehttp/ringphp\": \"<1.1.1\",\n        \"guzzlehttp/guzzle\": \"<7.0\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ]\n}\n"
  },
  {
    "path": "src/AzureBlobStorage/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n**/*Test.php export-ignore\n**/*Stub.php export-ignore\nREADME.md export-ignore\n"
  },
  {
    "path": "src/AzureBlobStorage/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/AzureBlobStorage/AzureBlobStorageAdapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AzureBlobStorage;\n\nuse DateTime;\nuse DateTimeInterface;\nuse League\\Flysystem\\ChecksumAlgoIsNotSupported;\nuse League\\Flysystem\\ChecksumProvider;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\UnableToCheckDirectoryExistence;\nuse League\\Flysystem\\UnableToCheckFileExistence;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToGenerateTemporaryUrl;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToProvideChecksum;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UrlGeneration\\PublicUrlGenerator;\nuse League\\Flysystem\\UrlGeneration\\TemporaryUrlGenerator;\nuse League\\MimeTypeDetection\\FinfoMimeTypeDetector;\nuse League\\MimeTypeDetection\\MimeTypeDetector;\nuse MicrosoftAzure\\Storage\\Blob\\BlobRestProxy;\nuse MicrosoftAzure\\Storage\\Blob\\BlobSharedAccessSignatureHelper;\nuse MicrosoftAzure\\Storage\\Blob\\Models\\BlobProperties;\nuse MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlockBlobOptions;\nuse MicrosoftAzure\\Storage\\Blob\\Models\\ListBlobsOptions;\nuse MicrosoftAzure\\Storage\\Common\\Exceptions\\ServiceException;\nuse MicrosoftAzure\\Storage\\Common\\Internal\\Resources;\nuse MicrosoftAzure\\Storage\\Common\\Internal\\StorageServiceSettings;\nuse MicrosoftAzure\\Storage\\Common\\Models\\ContinuationToken;\nuse Throwable;\nuse function base64_decode;\nuse function bin2hex;\nuse function stream_get_contents;\n\nclass AzureBlobStorageAdapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider, TemporaryUrlGenerator\n{\n    /** @var string[] */\n    private const META_OPTIONS = [\n        'CacheControl',\n        'ContentType',\n        'Metadata',\n        'ContentLanguage',\n        'ContentEncoding',\n    ];\n    const ON_VISIBILITY_THROW_ERROR = 'throw';\n    const ON_VISIBILITY_IGNORE = 'ignore';\n\n    private MimeTypeDetector $mimeTypeDetector;\n    private PathPrefixer $prefixer;\n\n    public function __construct(\n        private BlobRestProxy $client,\n        private string $container,\n        string $prefix = '',\n        ?MimeTypeDetector $mimeTypeDetector = null,\n        private int $maxResultsForContentsListing = 5000,\n        private string $visibilityHandling = self::ON_VISIBILITY_THROW_ERROR,\n        private ?StorageServiceSettings $serviceSettings = null,\n    ) {\n        $this->prefixer = new PathPrefixer($prefix);\n        $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        $resolvedDestination = $this->prefixer->prefixPath($destination);\n        $resolvedSource = $this->prefixer->prefixPath($source);\n\n        try {\n            $this->client->copyBlob(\n                $this->container,\n                $resolvedDestination,\n                $this->container,\n                $resolvedSource\n            );\n        } catch (Throwable $throwable) {\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $throwable);\n        }\n    }\n\n    public function delete(string $path): void\n    {\n        $location = $this->prefixer->prefixPath($path);\n\n        try {\n            $this->client->deleteBlob($this->container, $location);\n        } catch (Throwable $exception) {\n            if ($exception instanceof ServiceException && $exception->getCode() === 404) {\n                return;\n            }\n\n            throw UnableToDeleteFile::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function read(string $path): string\n    {\n        $response = $this->readStream($path);\n\n        return stream_get_contents($response);\n    }\n\n    public function readStream(string $path)\n    {\n        $location = $this->prefixer->prefixPath($path);\n\n        try {\n            $response = $this->client->getBlob($this->container, $location);\n\n            return $response->getContentStream();\n        } catch (Throwable $exception) {\n            throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function listContents(string $path, bool $deep = false): iterable\n    {\n        $resolved = $this->prefixer->prefixDirectoryPath($path);\n\n        $options = new ListBlobsOptions();\n        $options->setPrefix($resolved);\n        $options->setMaxResults($this->maxResultsForContentsListing);\n\n        if ($deep === false) {\n            $options->setDelimiter('/');\n        }\n\n        do {\n            $response = $this->client->listBlobs($this->container, $options);\n\n            foreach ($response->getBlobPrefixes() as $blobPrefix) {\n                yield new DirectoryAttributes($this->prefixer->stripDirectoryPrefix($blobPrefix->getName()));\n            }\n\n            foreach ($response->getBlobs() as $blob) {\n                yield $this->normalizeBlobProperties(\n                    $this->prefixer->stripPrefix($blob->getName()),\n                    $blob->getProperties()\n                );\n            }\n\n            $continuationToken = $response->getContinuationToken();\n            $options->setContinuationToken($continuationToken);\n        } while ($continuationToken instanceof ContinuationToken);\n    }\n\n    public function fileExists(string $path): bool\n    {\n        $resolved = $this->prefixer->prefixPath($path);\n        try {\n            return $this->fetchMetadata($resolved) !== null;\n        } catch (Throwable $exception) {\n            if ($exception instanceof ServiceException && $exception->getCode() === 404) {\n                return false;\n            }\n            throw UnableToCheckFileExistence::forLocation($path, $exception);\n        }\n    }\n\n    public function directoryExists(string $path): bool\n    {\n        $resolved = $this->prefixer->prefixDirectoryPath($path);\n        $options = new ListBlobsOptions();\n        $options->setPrefix($resolved);\n        $options->setMaxResults(1);\n\n        try {\n            $listResults = $this->client->listBlobs($this->container, $options);\n\n            return count($listResults->getBlobs()) > 0;\n        } catch (Throwable $exception) {\n            throw UnableToCheckDirectoryExistence::forLocation($path, $exception);\n        }\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        $resolved = $this->prefixer->prefixDirectoryPath($path);\n        $options = new ListBlobsOptions();\n        $options->setPrefix($resolved);\n\n        try {\n            start:\n            $listResults = $this->client->listBlobs($this->container, $options);\n\n            foreach ($listResults->getBlobs() as $blob) {\n                $this->client->deleteBlob($this->container, $blob->getName());\n            }\n\n            $continuationToken = $listResults->getContinuationToken();\n\n            if ($continuationToken instanceof ContinuationToken) {\n                $options->setContinuationToken($continuationToken);\n                goto start;\n            }\n        } catch (Throwable $exception) {\n            throw UnableToDeleteDirectory::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        // this is not supported by Azure\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        if ($this->visibilityHandling === self::ON_VISIBILITY_THROW_ERROR) {\n            throw UnableToSetVisibility::atLocation($path, 'Azure does not support this operation.');\n        }\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        throw UnableToRetrieveMetadata::visibility($path, 'Azure does not support visibility');\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        try {\n            return $this->fetchMetadata($this->prefixer->prefixPath($path));\n        } catch (Throwable $exception) {\n            throw UnableToRetrieveMetadata::mimeType($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        try {\n            return $this->fetchMetadata($this->prefixer->prefixPath($path));\n        } catch (Throwable $exception) {\n            throw UnableToRetrieveMetadata::lastModified($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        try {\n            return $this->fetchMetadata($this->prefixer->prefixPath($path));\n        } catch (Throwable $exception) {\n            throw UnableToRetrieveMetadata::fileSize($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        try {\n            $this->copy($source, $destination, $config);\n            $this->delete($source);\n        } catch (Throwable $exception) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        $this->upload($path, $contents, $config);\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        $this->upload($path, $contents, $config);\n    }\n\n    /**\n     * @param string|resource $contents\n     */\n    private function upload(string $destination, $contents, Config $config): void\n    {\n        $resolved = $this->prefixer->prefixPath($destination);\n        try {\n            $options = $this->getOptionsFromConfig($config);\n\n            if (empty($options->getContentType())) {\n                $options->setContentType($this->mimeTypeDetector->detectMimeType($resolved, $contents));\n            }\n\n            $this->client->createBlockBlob(\n                $this->container,\n                $resolved,\n                $contents,\n                $options\n            );\n        } catch (Throwable $exception) {\n            throw UnableToWriteFile::atLocation($destination, $exception->getMessage(), $exception);\n        }\n    }\n\n    private function fetchMetadata(string $path): FileAttributes\n    {\n        return $this->normalizeBlobProperties(\n            $path,\n            $this->client->getBlobProperties($this->container, $path)->getProperties()\n        );\n    }\n\n    private function getOptionsFromConfig(Config $config): CreateBlockBlobOptions\n    {\n        $options = new CreateBlockBlobOptions();\n\n        foreach (self::META_OPTIONS as $option) {\n            $setting = $config->get($option, '___NOT__SET___');\n\n            if ($setting === '___NOT__SET___') {\n                continue;\n            }\n\n            call_user_func([$options, \"set$option\"], $setting);\n        }\n\n        $mimeType = $config->get('mimetype');\n\n        if ($mimeType !== null) {\n            $options->setContentType($mimeType);\n        }\n\n        return $options;\n    }\n\n    private function normalizeBlobProperties(string $path, BlobProperties $properties): FileAttributes\n    {\n        return new FileAttributes(\n            $path,\n            $properties->getContentLength(),\n            null,\n            $properties->getLastModified()->getTimestamp(),\n            $properties->getContentType(),\n            ['md5_checksum' => $properties->getContentMD5()]\n        );\n    }\n\n    public function publicUrl(string $path, Config $config): string\n    {\n        $location = $this->prefixer->prefixPath($path);\n\n        return $this->client->getBlobUrl($this->container, $location);\n    }\n\n    public function checksum(string $path, Config $config): string\n    {\n        $algo = $config->get('checksum_algo', 'md5');\n\n        if ($algo !== 'md5') {\n            throw new ChecksumAlgoIsNotSupported();\n        }\n\n        try {\n            $metadata = $this->fetchMetadata($this->prefixer->prefixPath($path));\n            $checksum = $metadata->extraMetadata()['md5_checksum'] ?? '__not_specified';\n        } catch (Throwable $exception) {\n            throw new UnableToProvideChecksum($exception->getMessage(), $path, $exception);\n        }\n\n        if ($checksum === '__not_specified') {\n            throw new UnableToProvideChecksum('No checksum provided in metadata', $path);\n        }\n\n        return bin2hex(base64_decode($checksum));\n    }\n\n    public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string\n    {\n        if ( ! $this->serviceSettings instanceof StorageServiceSettings) {\n            throw UnableToGenerateTemporaryUrl::noGeneratorConfigured(\n                $path,\n                'The $serviceSettings constructor parameter must be set to generate temporary URLs.',\n            );\n        }\n\n        try {\n            $sas = new BlobSharedAccessSignatureHelper($this->serviceSettings->getName(), $this->serviceSettings->getKey());\n            $baseUrl = $this->publicUrl($path, $config);\n            $resourceName = $this->container . '/' . ltrim($this->prefixer->prefixPath($path), '/');\n            $token = $sas->generateBlobServiceSharedAccessSignatureToken(\n                Resources::RESOURCE_TYPE_BLOB,\n                $resourceName,\n                'r', // read\n                DateTime::createFromInterface($expiresAt),\n                $config->get('signed_start', ''),\n                $config->get('signed_ip', ''),\n                $config->get('signed_protocol', 'https'),\n                $config->get('signed_identifier', ''),\n                $config->get('cache_control', ''),\n                $config->get('content_disposition', $config->get('content_deposition', '')),\n                $config->get('content_encoding', ''),\n                $config->get('content_language', ''),\n                $config->get('content_type', ''),\n            );\n\n            return \"$baseUrl?$token\";\n        } catch (Throwable $exception) {\n            throw UnableToGenerateTemporaryUrl::dueToError($path, $exception);\n        }\n    }\n}\n"
  },
  {
    "path": "src/AzureBlobStorage/AzureBlobStorageAdapterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\AzureBlobStorage;\n\nuse League\\Flysystem\\AdapterTestUtilities\\FilesystemAdapterTestCase as TestCase;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\Visibility;\nuse MicrosoftAzure\\Storage\\Blob\\BlobRestProxy;\nuse MicrosoftAzure\\Storage\\Common\\Internal\\StorageServiceSettings;\nuse function getenv;\n\n/**\n * @group azure\n */\nclass AzureBlobStorageAdapterTest extends TestCase\n{\n    const CONTAINER_NAME = 'flysystem';\n\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        $dsn = getenv('FLYSYSTEM_AZURE_DSN');\n\n        if (empty($dsn)) {\n            self::markTestSkipped('FLYSYSTEM_AZURE_DSN is not provided.');\n        }\n\n        $client = BlobRestProxy::createBlobService($dsn);\n        $serviceSettings = StorageServiceSettings::createFromConnectionString($dsn);\n\n        return new AzureBlobStorageAdapter(\n            $client,\n            self::CONTAINER_NAME,\n            'ci',\n            serviceSettings: $serviceSettings,\n        );\n    }\n\n    /**\n     * @test\n     */\n    public function overwriting_a_file(): void\n    {\n        $this->runScenario(\n            function () {\n                $this->givenWeHaveAnExistingFile('path.txt', 'contents');\n                $adapter = $this->adapter();\n\n                $adapter->write('path.txt', 'new contents', new Config());\n\n                $contents = $adapter->read('path.txt');\n                $this->assertEquals('new contents', $contents);\n            }\n        );\n    }\n\n    /**\n     * @test\n     */\n    public function setting_visibility(): void\n    {\n        self::markTestSkipped('Azure does not support visibility');\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_set_visibility(): void\n    {\n        self::markTestSkipped('Azure does not support visibility');\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_check_visibility(): void\n    {\n        self::markTestSkipped('Azure does not support visibility');\n    }\n\n    public function fetching_unknown_mime_type_of_a_file(): void\n    {\n        $this->markTestSkipped('This adapter always returns a mime-type');\n    }\n\n    public function listing_contents_recursive(): void\n    {\n        $this->markTestSkipped('This adapter does not support creating directories');\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be copied',\n                new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])\n            );\n\n            $adapter->copy('source.txt', 'destination.txt', new Config());\n\n            $this->assertTrue($adapter->fileExists('source.txt'));\n            $this->assertTrue($adapter->fileExists('destination.txt'));\n            $this->assertEquals('contents to be copied', $adapter->read('destination.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function moving_a_file(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be copied',\n                new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])\n            );\n            $adapter->move('source.txt', 'destination.txt', new Config());\n            $this->assertFalse(\n                $adapter->fileExists('source.txt'),\n                'After moving a file should no longer exist in the original location.'\n            );\n            $this->assertTrue(\n                $adapter->fileExists('destination.txt'),\n                'After moving, a file should be present at the new location.'\n            );\n            $this->assertEquals('contents to be copied', $adapter->read('destination.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file_again(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be copied',\n                new Config()\n            );\n\n            $adapter->copy('source.txt', 'destination.txt', new Config());\n\n            $this->assertTrue($adapter->fileExists('source.txt'));\n            $this->assertTrue($adapter->fileExists('destination.txt'));\n            $this->assertEquals('contents to be copied', $adapter->read('destination.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function setting_visibility_can_be_ignored_not_supported(): void\n    {\n        $this->givenWeHaveAnExistingFile('some-file.md');\n        $this->expectNotToPerformAssertions();\n\n        $client = BlobRestProxy::createBlobService(getenv('FLYSYSTEM_AZURE_DSN'));\n        $adapter = new AzureBlobStorageAdapter($client, self::CONTAINER_NAME, 'ci', null, 50000, AzureBlobStorageAdapter::ON_VISIBILITY_IGNORE);\n\n        $adapter->setVisibility('some-file.md', 'public');\n    }\n\n    /**\n     * @test\n     */\n    public function setting_visibility_causes_errors(): void\n    {\n        $this->givenWeHaveAnExistingFile('some-file.md');\n        $adapter = $this->adapter();\n\n        $this->expectException(UnableToSetVisibility::class);\n\n        $adapter->setVisibility('some-file.md', 'public');\n    }\n\n    /**\n     * @test\n     */\n    public function checking_if_a_directory_exists_after_creating_it(): void\n    {\n        $this->markTestSkipped('This adapter does not support creating directories');\n    }\n\n    /**\n     * @test\n     */\n    public function setting_visibility_on_a_file_that_does_not_exist(): void\n    {\n        $this->markTestSkipped('This adapter does not support visibility');\n    }\n\n    /**\n     * @test\n     */\n    public function creating_a_directory(): void\n    {\n        $this->markTestSkipped('This adapter does not support creating directories');\n    }\n}\n"
  },
  {
    "path": "src/AzureBlobStorage/LICENSE",
    "content": "Copyright (c) 2013-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/AzureBlobStorage/README.md",
    "content": "## Sub-split for Flysystem's Azure Blob Storage Adapter\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/AzureBlobStorage/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-azure-blob-storage\",\n    \"autoload\": {\n        \"psr-4\": {\n            \"League\\\\Flysystem\\\\AzureBlobStorage\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"league/flysystem\": \"^3.10.0\",\n        \"microsoft/azure-storage-blob\": \"^1.1\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ],\n    \"abandoned\": \"azure-oss/storage-blob-flysystem\"\n}\n"
  },
  {
    "path": "src/CalculateChecksumFromStream.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse function hash_final;\nuse function hash_init;\nuse function hash_update_stream;\n\ntrait CalculateChecksumFromStream\n{\n    private function calculateChecksumFromStream(string $path, Config $config): string\n    {\n        try {\n            $stream = $this->readStream($path);\n            $algo = (string) $config->get('checksum_algo', 'md5');\n            $context = hash_init($algo);\n            hash_update_stream($context, $stream);\n\n            return hash_final($context);\n        } catch (FilesystemException $exception) {\n            throw new UnableToProvideChecksum($exception->getMessage(), $path, $exception);\n        }\n    }\n\n    /**\n     * @return resource\n     */\n    abstract public function readStream(string $path);\n}\n"
  },
  {
    "path": "src/ChecksumAlgoIsNotSupported.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse InvalidArgumentException;\n\nfinal class ChecksumAlgoIsNotSupported extends InvalidArgumentException\n{\n\n}\n"
  },
  {
    "path": "src/ChecksumProvider.php",
    "content": "<?php\n\nnamespace League\\Flysystem;\n\ninterface ChecksumProvider\n{\n    /**\n     * @return string MD5 hash of the file contents\n     *\n     * @throws UnableToProvideChecksum\n     * @throws ChecksumAlgoIsNotSupported\n     */\n    public function checksum(string $path, Config $config): string;\n}\n"
  },
  {
    "path": "src/Config.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse function array_diff_key;\nuse function array_flip;\nuse function array_merge;\n\nclass Config\n{\n    public const OPTION_COPY_IDENTICAL_PATH = 'copy_destination_same_as_source';\n    public const OPTION_MOVE_IDENTICAL_PATH = 'move_destination_same_as_source';\n    public const OPTION_VISIBILITY = 'visibility';\n    public const OPTION_DIRECTORY_VISIBILITY = 'directory_visibility';\n    public const OPTION_RETAIN_VISIBILITY = 'retain_visibility';\n\n    public function __construct(private array $options = [])\n    {\n    }\n\n    /**\n     * @param mixed $default\n     *\n     * @return mixed\n     */\n    public function get(string $property, $default = null)\n    {\n        return $this->options[$property] ?? $default;\n    }\n\n    public function extend(array $options): Config\n    {\n        return new Config(array_merge($this->options, $options));\n    }\n\n    public function withDefaults(array $defaults): Config\n    {\n        return new Config($this->options + $defaults);\n    }\n\n    public function toArray(): array\n    {\n        return $this->options;\n    }\n\n    public function withSetting(string $property, mixed $setting): Config\n    {\n        return $this->extend([$property => $setting]);\n    }\n\n    public function withoutSettings(string ...$settings): Config\n    {\n        return new Config(array_diff_key($this->options, array_flip($settings)));\n    }\n}\n"
  },
  {
    "path": "src/ConfigTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse PHPUnit\\Framework\\TestCase;\n\nclass ConfigTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function a_config_object_exposes_passed_options(): void\n    {\n        $config = new Config(['option' => 'value']);\n        $this->assertEquals('value', $config->get('option'));\n    }\n\n    /**\n     * @test\n     */\n    public function a_config_object_returns_a_default_value(): void\n    {\n        $config = new Config();\n\n        $this->assertNull($config->get('option'));\n        $this->assertEquals('default', $config->get('option', 'default'));\n    }\n\n    /**\n     * @test\n     */\n    public function extending_a_config_with_options(): void\n    {\n        $config = new Config(['option' => 'value', 'first' => 1]);\n        $extended = $config->extend(['option' => 'overwritten', 'second' => 2]);\n\n        $this->assertEquals('overwritten', $extended->get('option'));\n        $this->assertEquals(1, $extended->get('first'));\n        $this->assertEquals(2, $extended->get('second'));\n    }\n\n    /**\n     * @test\n     */\n    public function extending_with_defaults(): void\n    {\n        $config = new Config(['option' => 'set']);\n\n        $withDefaults = $config->withDefaults(['option' => 'default', 'other' => 'default']);\n\n        $this->assertEquals('set', $withDefaults->get('option'));\n        $this->assertEquals('default', $withDefaults->get('other'));\n    }\n\n    /**\n     * @test\n     */\n    public function extending_without_settings(): void\n    {\n        // arrange\n        $config = new Config(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]);\n\n        // act\n        $withoutSetting = $config->withoutSettings('b', 'd');\n\n        // assert\n        $this->assertEquals(['a' => 1, 'c' => 3], $withoutSetting->toArray());\n    }\n}\n"
  },
  {
    "path": "src/CorruptedPathDetected.php",
    "content": "<?php\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\n\nfinal class CorruptedPathDetected extends RuntimeException implements FilesystemException\n{\n    public static function forPath(string $path): CorruptedPathDetected\n    {\n        return new CorruptedPathDetected(\"Corrupted path detected: \" . $path);\n    }\n}\n"
  },
  {
    "path": "src/DecoratedAdapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nabstract class DecoratedAdapter implements FilesystemAdapter\n{\n    public function __construct(protected FilesystemAdapter $adapter)\n    {\n    }\n\n    public function fileExists(string $path): bool\n    {\n        return $this->adapter->fileExists($path);\n    }\n\n    public function directoryExists(string $path): bool\n    {\n        return $this->adapter->directoryExists($path);\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        $this->adapter->write($path, $contents, $config);\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        $this->adapter->writeStream($path, $contents, $config);\n    }\n\n    public function read(string $path): string\n    {\n        return $this->adapter->read($path);\n    }\n\n    public function readStream(string $path)\n    {\n        return $this->adapter->readStream($path);\n    }\n\n    public function delete(string $path): void\n    {\n        $this->adapter->delete($path);\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        $this->adapter->deleteDirectory($path);\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        $this->adapter->createDirectory($path, $config);\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        $this->adapter->setVisibility($path, $visibility);\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        return $this->adapter->visibility($path);\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        return $this->adapter->mimeType($path);\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        return $this->adapter->lastModified($path);\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        return $this->adapter->fileSize($path);\n    }\n\n    public function listContents(string $path, bool $deep): iterable\n    {\n        return $this->adapter->listContents($path, $deep);\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        $this->adapter->move($source, $destination, $config);\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        $this->adapter->copy($source, $destination, $config);\n    }\n}\n"
  },
  {
    "path": "src/DirectoryAttributes.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nclass DirectoryAttributes implements StorageAttributes\n{\n    use ProxyArrayAccessToProperties;\n    private string $type = StorageAttributes::TYPE_DIRECTORY;\n\n    public function __construct(\n        private string $path,\n        private ?string $visibility = null,\n        private ?int $lastModified = null,\n        private array $extraMetadata = []\n    ) {\n        $this->path = trim($this->path, '/');\n    }\n\n    public function path(): string\n    {\n        return $this->path;\n    }\n\n    public function type(): string\n    {\n        return $this->type;\n    }\n\n    public function visibility(): ?string\n    {\n        return $this->visibility;\n    }\n\n    public function lastModified(): ?int\n    {\n        return $this->lastModified;\n    }\n\n    public function extraMetadata(): array\n    {\n        return $this->extraMetadata;\n    }\n\n    public function isFile(): bool\n    {\n        return false;\n    }\n\n    public function isDir(): bool\n    {\n        return true;\n    }\n\n    public function withPath(string $path): self\n    {\n        $clone = clone $this;\n        $clone->path = $path;\n\n        return $clone;\n    }\n\n    public static function fromArray(array $attributes): self\n    {\n        return new DirectoryAttributes(\n            $attributes[StorageAttributes::ATTRIBUTE_PATH],\n            $attributes[StorageAttributes::ATTRIBUTE_VISIBILITY] ?? null,\n            $attributes[StorageAttributes::ATTRIBUTE_LAST_MODIFIED] ?? null,\n            $attributes[StorageAttributes::ATTRIBUTE_EXTRA_METADATA] ?? []\n        );\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function jsonSerialize(): array\n    {\n        return [\n            StorageAttributes::ATTRIBUTE_TYPE => $this->type,\n            StorageAttributes::ATTRIBUTE_PATH => $this->path,\n            StorageAttributes::ATTRIBUTE_VISIBILITY => $this->visibility,\n            StorageAttributes::ATTRIBUTE_LAST_MODIFIED => $this->lastModified,\n            StorageAttributes::ATTRIBUTE_EXTRA_METADATA => $this->extraMetadata,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/DirectoryAttributesTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse PHPUnit\\Framework\\TestCase;\n\n/**\n * @group core\n */\nclass DirectoryAttributesTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function exposing_some_values(): void\n    {\n        $attrs = new DirectoryAttributes('some/path');\n        $this->assertTrue($attrs->isDir());\n        $this->assertFalse($attrs->isFile());\n        $this->assertEquals(StorageAttributes::TYPE_DIRECTORY, $attrs->type());\n        $this->assertEquals('some/path', $attrs->path());\n        $this->assertNull($attrs->visibility());\n    }\n\n    /**\n     * @test\n     */\n    public function exposing_visibility(): void\n    {\n        $attrs = new DirectoryAttributes('some/path', Visibility::PRIVATE);\n        $this->assertEquals(Visibility::PRIVATE, $attrs->visibility());\n    }\n\n    /**\n     * @test\n     */\n    public function exposing_last_modified(): void\n    {\n        $attrs = new DirectoryAttributes('some/path', null, $timestamp = time());\n        $this->assertEquals($timestamp, $attrs->lastModified());\n    }\n\n    /**\n     * @test\n     */\n    public function exposing_extra_meta_data(): void\n    {\n        $attrs = new DirectoryAttributes('some/path', null, null, ['key' => 'value']);\n        $this->assertEquals(['key' => 'value'], $attrs->extraMetadata());\n    }\n\n    /**\n     * @test\n     */\n    public function serialization_capabilities(): void\n    {\n        $attrs = new DirectoryAttributes('some/path');\n        $payload = $attrs->jsonSerialize();\n        $attrsFromPayload = DirectoryAttributes::fromArray($payload);\n        $this->assertEquals($attrs, $attrsFromPayload);\n    }\n}\n"
  },
  {
    "path": "src/DirectoryListing.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse ArrayIterator;\nuse Generator;\nuse IteratorAggregate;\nuse Traversable;\n\n/**\n * @template T\n */\nclass DirectoryListing implements IteratorAggregate\n{\n    /**\n     * @param iterable<T> $listing\n     */\n    public function __construct(private iterable $listing)\n    {\n    }\n\n    /**\n     * @param callable(T): bool $filter\n     *\n     * @return DirectoryListing<T>\n     */\n    public function filter(callable $filter): DirectoryListing\n    {\n        $generator = (static function (iterable $listing) use ($filter): Generator {\n            foreach ($listing as $item) {\n                if ($filter($item)) {\n                    yield $item;\n                }\n            }\n        })($this->listing);\n\n        return new DirectoryListing($generator);\n    }\n\n    /**\n     * @template R\n     *\n     * @param callable(T): R $mapper\n     *\n     * @return DirectoryListing<R>\n     */\n    public function map(callable $mapper): DirectoryListing\n    {\n        $generator = (static function (iterable $listing) use ($mapper): Generator {\n            foreach ($listing as $item) {\n                yield $mapper($item);\n            }\n        })($this->listing);\n\n        return new DirectoryListing($generator);\n    }\n\n    /**\n     * @return DirectoryListing<T>\n     */\n    public function sortByPath(): DirectoryListing\n    {\n        $listing = $this->toArray();\n\n        usort($listing, function (StorageAttributes $a, StorageAttributes $b) {\n            return $a->path() <=> $b->path();\n        });\n\n        return new DirectoryListing($listing);\n    }\n\n    /**\n     * @return Traversable<T>\n     */\n    public function getIterator(): Traversable\n    {\n        return $this->listing instanceof Traversable\n            ? $this->listing\n            : new ArrayIterator($this->listing);\n    }\n\n    /**\n     * @return T[]\n     */\n    public function toArray(): array\n    {\n        return $this->listing instanceof Traversable\n            ? iterator_to_array($this->listing, false)\n            : (array) $this->listing;\n    }\n}\n"
  },
  {
    "path": "src/DirectoryListingTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse Generator;\nuse PHPUnit\\Framework\\TestCase;\n\nuse function iterator_to_array;\n\n/**\n * @group core\n */\nclass DirectoryListingTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function mapping_a_listing(): void\n    {\n        $numbers = $this->generateIntegers(1, 10);\n        $listing = new DirectoryListing($numbers);\n\n        $mappedListing = $listing->map(function (int $i) {\n            return $i * 2;\n        });\n        $mappedNumbers = $mappedListing->toArray();\n\n        $expectedNumbers = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20];\n        $this->assertEquals($expectedNumbers, $mappedNumbers);\n    }\n\n    /**\n     * @test\n     */\n    public function mapping_a_listing_twice(): void\n    {\n        $numbers = $this->generateIntegers(1, 10);\n        $listing = new DirectoryListing($numbers);\n\n        $mappedListing = $listing->map(function (int $i) {\n            return $i * 2;\n        });\n        $mappedListing = $mappedListing->map(function (int $i) {\n            return $i / 2;\n        });\n        $mappedNumbers = $mappedListing->toArray();\n\n        $expectedNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];\n        $this->assertEquals($expectedNumbers, $mappedNumbers);\n    }\n\n    /**\n     * @test\n     */\n    public function filter_a_listing(): void\n    {\n        $numbers = $this->generateIntegers(1, 20);\n        $listing = new DirectoryListing($numbers);\n\n        $fileredListing = $listing->filter(function (int $i) {\n            return $i % 2 === 0;\n        });\n        $mappedNumbers = $fileredListing->toArray();\n\n        $expectedNumbers = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20];\n        $this->assertEquals($expectedNumbers, $mappedNumbers);\n    }\n\n    /**\n     * @test\n     */\n    public function filter_a_listing_twice(): void\n    {\n        $numbers = $this->generateIntegers(1, 20);\n        $listing = new DirectoryListing($numbers);\n\n        $filteredListing = $listing->filter(function (int $i) {\n            return $i % 2 === 0;\n        });\n        $filteredListing = $filteredListing->filter(function (int $i) {\n            return $i > 10;\n        });\n        $mappedNumbers = $filteredListing->toArray();\n\n        $expectedNumbers = [12, 14, 16, 18, 20];\n        $this->assertEquals($expectedNumbers, $mappedNumbers);\n    }\n\n    /**\n     * @test\n     */\n    public function sorting_a_directory_listing(): void\n    {\n        $expected = ['a/a/a.txt', 'b/c/a.txt', 'c/b/a.txt', 'c/c/a.txt'];\n        $listing = new DirectoryListing([\n            new FileAttributes('b/c/a.txt'),\n            new FileAttributes('c/c/a.txt'),\n            new FileAttributes('c/b/a.txt'),\n            new FileAttributes('a/a/a.txt'),\n        ]);\n\n        $actual = $listing->sortByPath()\n            ->map(function ($i) {\n                return $i->path();\n            })\n            ->toArray();\n\n        self::assertEquals($expected, $actual);\n    }\n\n    /**\n     * @test\n     *\n     * @description this ensures that the output of a sorted listing is iterable\n     *\n     * @see https://github.com/thephpleague/flysystem/issues/1342\n     */\n    public function iterating_over_storted_output(): void\n    {\n        $listing = new DirectoryListing([\n            new FileAttributes('b/c/a.txt'),\n            new FileAttributes('c/c/a.txt'),\n            new FileAttributes('c/b/a.txt'),\n            new FileAttributes('a/a/a.txt'),\n        ]);\n\n        self::expectNotToPerformAssertions();\n\n        iterator_to_array($listing->sortByPath());\n    }\n\n    /**\n     * @return Generator<int>\n     */\n    private function generateIntegers(int $min, int $max): Generator\n    {\n        for ($i = $min; $i <= $max; $i++) {\n            yield $i;\n        }\n    }\n}\n"
  },
  {
    "path": "src/ExceptionInformationTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse PHPUnit\\Framework\\TestCase;\n\n/**\n * @group core\n */\nclass ExceptionInformationTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function copy_exception_information(): void\n    {\n        $exception = UnableToCopyFile::fromLocationTo('from', 'to');\n        $this->assertEquals('from', $exception->source());\n        $this->assertEquals('to', $exception->destination());\n        $this->assertEquals(FilesystemOperationFailed::OPERATION_COPY, $exception->operation());\n    }\n\n    /**\n     * @test\n     */\n    public function create_directory_exception_information(): void\n    {\n        $exception = UnableToCreateDirectory::atLocation('from', 'some message');\n        $this->assertEquals('from', $exception->location());\n        $this->assertStringContainsString('some message', $exception->getMessage());\n        $this->assertEquals(FilesystemOperationFailed::OPERATION_CREATE_DIRECTORY, $exception->operation());\n    }\n\n    /**\n     * @test\n     */\n    public function delete_directory_exception_information(): void\n    {\n        $exception = UnableToDeleteDirectory::atLocation('from', 'some message');\n        $this->assertEquals('some message', $exception->reason());\n        $this->assertEquals('from', $exception->location());\n        $this->assertStringContainsString('some message', $exception->getMessage());\n        $this->assertEquals(FilesystemOperationFailed::OPERATION_DELETE_DIRECTORY, $exception->operation());\n    }\n\n    /**\n     * @test\n     */\n    public function delete_file_exception_information(): void\n    {\n        $exception = UnableToDeleteFile::atLocation('from', 'some message');\n        $this->assertEquals('from', $exception->location());\n        $this->assertEquals('some message', $exception->reason());\n        $this->assertStringContainsString('some message', $exception->getMessage());\n        $this->assertEquals(FilesystemOperationFailed::OPERATION_DELETE, $exception->operation());\n    }\n\n    /**\n     * @test\n     */\n    public function unable_to_check_for_file_existence(): void\n    {\n        $exception = UnableToCheckFileExistence::forLocation('location');\n        $this->assertEquals(FilesystemOperationFailed::OPERATION_FILE_EXISTS, $exception->operation());\n    }\n\n    /**\n     * @test\n     */\n    public function unable_to_check_for_existence(): void\n    {\n        $exception = UnableToCheckExistence::forLocation('location');\n        $this->assertEquals(FilesystemOperationFailed::OPERATION_EXISTENCE_CHECK, $exception->operation());\n    }\n\n    /**\n     * @test\n     */\n    public function unable_to_check_for_directory_existence(): void\n    {\n        $exception = UnableToCheckDirectoryExistence::forLocation('location');\n        $this->assertEquals(FilesystemOperationFailed::OPERATION_DIRECTORY_EXISTS, $exception->operation());\n    }\n\n    /**\n     * @test\n     */\n    public function move_file_exception_information(): void\n    {\n        $exception = UnableToMoveFile::fromLocationTo('from', 'to');\n        $this->assertEquals('from', $exception->source());\n        $this->assertEquals('to', $exception->destination());\n        $this->assertEquals(FilesystemOperationFailed::OPERATION_MOVE, $exception->operation());\n    }\n\n    /**\n     * @test\n     */\n    public function read_file_exception_information(): void\n    {\n        $exception = UnableToReadFile::fromLocation('from', 'some message');\n        $this->assertEquals('from', $exception->location());\n        $this->assertEquals('some message', $exception->reason());\n        $this->assertStringContainsString('some message', $exception->getMessage());\n        $this->assertEquals(FilesystemOperationFailed::OPERATION_READ, $exception->operation());\n    }\n\n    /**\n     * @test\n     */\n    public function retrieve_visibility_exception_information(): void\n    {\n        $exception = UnableToRetrieveMetadata::visibility('from', 'some message');\n        $this->assertEquals('from', $exception->location());\n        $this->assertEquals(FileAttributes::ATTRIBUTE_VISIBILITY, $exception->metadataType());\n        $this->assertStringContainsString('some message', $exception->getMessage());\n        $this->assertEquals(FilesystemOperationFailed::OPERATION_RETRIEVE_METADATA, $exception->operation());\n    }\n\n    /**\n     * @test\n     */\n    public function set_visibility_exception_information(): void\n    {\n        $exception = UnableToSetVisibility::atLocation('from', 'some message');\n        $this->assertEquals('from', $exception->location());\n        $this->assertEquals('some message', $exception->reason());\n        $this->assertStringContainsString('some message', $exception->getMessage());\n        $this->assertEquals(FilesystemOperationFailed::OPERATION_SET_VISIBILITY, $exception->operation());\n    }\n\n    /**\n     * @test\n     */\n    public function write_file_exception_information(): void\n    {\n        $exception = UnableToWriteFile::atLocation('from', 'some message');\n        $this->assertEquals('from', $exception->location());\n        $this->assertEquals('some message', $exception->reason());\n        $this->assertStringContainsString('some message', $exception->getMessage());\n        $this->assertEquals(FilesystemOperationFailed::OPERATION_WRITE, $exception->operation());\n    }\n\n    /**\n     * @test\n     */\n    public function unreadable_file_exception_information(): void\n    {\n        $exception = UnreadableFileEncountered::atLocation('the-location');\n        $this->assertEquals('the-location', $exception->location());\n        $this->assertStringContainsString('the-location', $exception->getMessage());\n    }\n\n    /**\n     * @test\n     */\n    public function symbolic_link_exception_information(): void\n    {\n        $exception = SymbolicLinkEncountered::atLocation('the-location');\n        $this->assertEquals('the-location', $exception->location());\n        $this->assertStringContainsString('the-location', $exception->getMessage());\n    }\n\n    /**\n     * @test\n     */\n    public function path_traversal_exception_information(): void\n    {\n        $exception = PathTraversalDetected::forPath('../path.txt');\n        $this->assertEquals('../path.txt', $exception->path());\n        $this->assertStringContainsString('../path.txt', $exception->getMessage());\n    }\n}\n"
  },
  {
    "path": "src/FileAttributes.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nclass FileAttributes implements StorageAttributes\n{\n    use ProxyArrayAccessToProperties;\n    private string $type = StorageAttributes::TYPE_FILE;\n\n    public function __construct(\n        private string $path,\n        private ?int $fileSize = null,\n        private ?string $visibility = null,\n        private ?int $lastModified = null,\n        private ?string $mimeType = null,\n        private array $extraMetadata = []\n    ) {\n        $this->path = ltrim($this->path, '/');\n    }\n\n    public function type(): string\n    {\n        return $this->type;\n    }\n\n    public function path(): string\n    {\n        return $this->path;\n    }\n\n    public function fileSize(): ?int\n    {\n        return $this->fileSize;\n    }\n\n    public function visibility(): ?string\n    {\n        return $this->visibility;\n    }\n\n    public function lastModified(): ?int\n    {\n        return $this->lastModified;\n    }\n\n    public function mimeType(): ?string\n    {\n        return $this->mimeType;\n    }\n\n    public function extraMetadata(): array\n    {\n        return $this->extraMetadata;\n    }\n\n    public function isFile(): bool\n    {\n        return true;\n    }\n\n    public function isDir(): bool\n    {\n        return false;\n    }\n\n    public function withPath(string $path): self\n    {\n        $clone = clone $this;\n        $clone->path = $path;\n\n        return $clone;\n    }\n\n    public static function fromArray(array $attributes): self\n    {\n        return new FileAttributes(\n            $attributes[StorageAttributes::ATTRIBUTE_PATH],\n            $attributes[StorageAttributes::ATTRIBUTE_FILE_SIZE] ?? null,\n            $attributes[StorageAttributes::ATTRIBUTE_VISIBILITY] ?? null,\n            $attributes[StorageAttributes::ATTRIBUTE_LAST_MODIFIED] ?? null,\n            $attributes[StorageAttributes::ATTRIBUTE_MIME_TYPE] ?? null,\n            $attributes[StorageAttributes::ATTRIBUTE_EXTRA_METADATA] ?? []\n        );\n    }\n\n    public function jsonSerialize(): array\n    {\n        return [\n            StorageAttributes::ATTRIBUTE_TYPE => self::TYPE_FILE,\n            StorageAttributes::ATTRIBUTE_PATH => $this->path,\n            StorageAttributes::ATTRIBUTE_FILE_SIZE => $this->fileSize,\n            StorageAttributes::ATTRIBUTE_VISIBILITY => $this->visibility,\n            StorageAttributes::ATTRIBUTE_LAST_MODIFIED => $this->lastModified,\n            StorageAttributes::ATTRIBUTE_MIME_TYPE => $this->mimeType,\n            StorageAttributes::ATTRIBUTE_EXTRA_METADATA => $this->extraMetadata,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/FileAttributesTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse Generator;\nuse PHPUnit\\Framework\\TestCase;\n\nuse RuntimeException;\n\nuse function time;\n\n/**\n * @group core\n */\nclass FileAttributesTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function exposing_some_values(): void\n    {\n        $attrs = new FileAttributes('path.txt');\n        $this->assertFalse($attrs->isDir());\n        $this->assertTrue($attrs->isFile());\n        $this->assertEquals('path.txt', $attrs->path());\n        $this->assertEquals(StorageAttributes::TYPE_FILE, $attrs->type());\n        $this->assertNull($attrs->visibility());\n        $this->assertNull($attrs->fileSize());\n        $this->assertNull($attrs->mimeType());\n        $this->assertNull($attrs->lastModified());\n    }\n\n    /**\n     * @test\n     */\n    public function exposing_all_values(): void\n    {\n        $attrs = new FileAttributes('path.txt', 1234, Visibility::PRIVATE, $now = time(), 'plain/text', ['key' => 'value']);\n        $this->assertEquals('path.txt', $attrs->path());\n        $this->assertEquals(StorageAttributes::TYPE_FILE, $attrs->type());\n        $this->assertEquals(Visibility::PRIVATE, $attrs->visibility());\n        $this->assertEquals(1234, $attrs->fileSize());\n        $this->assertEquals($now, $attrs->lastModified());\n        $this->assertEquals('plain/text', $attrs->mimeType());\n        $this->assertEquals(['key' => 'value'], $attrs->extraMetadata());\n    }\n\n    /**\n     * @test\n     */\n    public function implements_array_access(): void\n    {\n        $attrs = new FileAttributes('path.txt', 1234, Visibility::PRIVATE, $now = time(), 'plain/text', ['key' => 'value']);\n        $this->assertEquals('path.txt', $attrs['path']);\n        $this->assertTrue(isset($attrs['path']));\n        $this->assertEquals(StorageAttributes::TYPE_FILE, $attrs['type']);\n        $this->assertEquals(Visibility::PRIVATE, $attrs['visibility']);\n        $this->assertEquals(1234, $attrs['file_size']);\n        $this->assertEquals($now, $attrs['last_modified']);\n        $this->assertEquals('plain/text', $attrs['mimeType']);\n        $this->assertEquals(['key' => 'value'], $attrs['extra_metadata']);\n    }\n\n    /**\n     * @test\n     */\n    public function properties_can_not_be_set(): void\n    {\n        $this->expectException(RuntimeException::class);\n        $attrs = new FileAttributes('path.txt');\n        $attrs['visibility'] = Visibility::PUBLIC;\n    }\n\n    /**\n     * @test\n     */\n    public function properties_can_not_be_unset(): void\n    {\n        $this->expectException(RuntimeException::class);\n        $attrs = new FileAttributes('path.txt');\n        unset($attrs['visibility']);\n    }\n\n    /**\n     * @dataProvider data_provider_for_json_transformation\n     *\n     * @test\n     */\n    public function json_transformations(FileAttributes $attributes): void\n    {\n        $payload = $attributes->jsonSerialize();\n        $newAttributes = FileAttributes::fromArray($payload);\n        $this->assertEquals($attributes, $newAttributes);\n    }\n\n    public static function data_provider_for_json_transformation(): Generator\n    {\n        yield [new FileAttributes('path.txt', 1234, Visibility::PRIVATE, $now = time(), 'plain/text', ['key' => 'value'])];\n        yield [new FileAttributes('another.txt')];\n    }\n}\n"
  },
  {
    "path": "src/Filesystem.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse DateTimeInterface;\nuse Generator;\nuse League\\Flysystem\\UrlGeneration\\PrefixPublicUrlGenerator;\nuse League\\Flysystem\\UrlGeneration\\PublicUrlGenerator;\nuse League\\Flysystem\\UrlGeneration\\ShardedPrefixPublicUrlGenerator;\nuse League\\Flysystem\\UrlGeneration\\TemporaryUrlGenerator;\nuse Throwable;\n\nuse function array_key_exists;\nuse function is_array;\n\nclass Filesystem implements FilesystemOperator\n{\n    use CalculateChecksumFromStream;\n\n    private Config $config;\n    private PathNormalizer $pathNormalizer;\n\n    public function __construct(\n        private FilesystemAdapter $adapter,\n        array $config = [],\n        ?PathNormalizer $pathNormalizer = null,\n        private ?PublicUrlGenerator $publicUrlGenerator = null,\n        private ?TemporaryUrlGenerator $temporaryUrlGenerator = null,\n    ) {\n        $this->config = new Config($config);\n        $this->pathNormalizer = $pathNormalizer ?? new WhitespacePathNormalizer();\n    }\n\n    public function fileExists(string $location): bool\n    {\n        return $this->adapter->fileExists($this->pathNormalizer->normalizePath($location));\n    }\n\n    public function directoryExists(string $location): bool\n    {\n        return $this->adapter->directoryExists($this->pathNormalizer->normalizePath($location));\n    }\n\n    public function has(string $location): bool\n    {\n        $path = $this->pathNormalizer->normalizePath($location);\n\n        return $this->adapter->fileExists($path) || $this->adapter->directoryExists($path);\n    }\n\n    public function write(string $location, string $contents, array $config = []): void\n    {\n        $this->adapter->write(\n            $this->pathNormalizer->normalizePath($location),\n            $contents,\n            $this->config->extend($config)\n        );\n    }\n\n    public function writeStream(string $location, $contents, array $config = []): void\n    {\n        /* @var resource $contents */\n        $this->assertIsResource($contents);\n        $this->rewindStream($contents);\n        $this->adapter->writeStream(\n            $this->pathNormalizer->normalizePath($location),\n            $contents,\n            $this->config->extend($config)\n        );\n    }\n\n    public function read(string $location): string\n    {\n        return $this->adapter->read($this->pathNormalizer->normalizePath($location));\n    }\n\n    public function readStream(string $location)\n    {\n        return $this->adapter->readStream($this->pathNormalizer->normalizePath($location));\n    }\n\n    public function delete(string $location): void\n    {\n        $this->adapter->delete($this->pathNormalizer->normalizePath($location));\n    }\n\n    public function deleteDirectory(string $location): void\n    {\n        $this->adapter->deleteDirectory($this->pathNormalizer->normalizePath($location));\n    }\n\n    public function createDirectory(string $location, array $config = []): void\n    {\n        $this->adapter->createDirectory(\n            $this->pathNormalizer->normalizePath($location),\n            $this->config->extend($config)\n        );\n    }\n\n    public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing\n    {\n        $path = $this->pathNormalizer->normalizePath($location);\n        $listing = $this->adapter->listContents($path, $deep);\n\n        return new DirectoryListing($this->pipeListing($location, $deep, $listing));\n    }\n\n    private function pipeListing(string $location, bool $deep, iterable $listing): Generator\n    {\n        try {\n            foreach ($listing as $item) {\n                yield $item;\n            }\n        } catch (Throwable $exception) {\n            throw UnableToListContents::atLocation($location, $deep, $exception);\n        }\n    }\n\n    public function move(string $source, string $destination, array $config = []): void\n    {\n        $config = $this->resolveConfigForMoveAndCopy($config);\n        $from = $this->pathNormalizer->normalizePath($source);\n        $to = $this->pathNormalizer->normalizePath($destination);\n\n        if ($from === $to) {\n            $resolutionStrategy = $config->get(Config::OPTION_MOVE_IDENTICAL_PATH, ResolveIdenticalPathConflict::TRY);\n\n            if ($resolutionStrategy === ResolveIdenticalPathConflict::FAIL) {\n                throw UnableToMoveFile::sourceAndDestinationAreTheSame($source, $destination);\n            } elseif ($resolutionStrategy === ResolveIdenticalPathConflict::IGNORE) {\n                return;\n            }\n        }\n\n        $this->adapter->move($from, $to, $config);\n    }\n\n    public function copy(string $source, string $destination, array $config = []): void\n    {\n        $config = $this->resolveConfigForMoveAndCopy($config);\n        $from = $this->pathNormalizer->normalizePath($source);\n        $to = $this->pathNormalizer->normalizePath($destination);\n\n        if ($from === $to) {\n            $resolutionStrategy = $config->get(Config::OPTION_COPY_IDENTICAL_PATH, ResolveIdenticalPathConflict::TRY);\n\n            if ($resolutionStrategy === ResolveIdenticalPathConflict::FAIL) {\n                throw UnableToCopyFile::sourceAndDestinationAreTheSame($source, $destination);\n            } elseif ($resolutionStrategy === ResolveIdenticalPathConflict::IGNORE) {\n                return;\n            }\n        }\n\n        $this->adapter->copy($from, $to, $config);\n    }\n\n    public function lastModified(string $path): int\n    {\n        return $this->adapter->lastModified($this->pathNormalizer->normalizePath($path))->lastModified();\n    }\n\n    public function fileSize(string $path): int\n    {\n        return $this->adapter->fileSize($this->pathNormalizer->normalizePath($path))->fileSize();\n    }\n\n    public function mimeType(string $path): string\n    {\n        return $this->adapter->mimeType($this->pathNormalizer->normalizePath($path))->mimeType();\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        $this->adapter->setVisibility($this->pathNormalizer->normalizePath($path), $visibility);\n    }\n\n    public function visibility(string $path): string\n    {\n        return $this->adapter->visibility($this->pathNormalizer->normalizePath($path))->visibility();\n    }\n\n    public function publicUrl(string $path, array $config = []): string\n    {\n        $this->publicUrlGenerator ??= $this->resolvePublicUrlGenerator()\n            ?? throw UnableToGeneratePublicUrl::noGeneratorConfigured($path);\n        $config = $this->config->extend($config);\n\n        return $this->publicUrlGenerator->publicUrl(\n            $this->pathNormalizer->normalizePath($path),\n            $config,\n        );\n    }\n\n    public function temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []): string\n    {\n        $generator = $this->temporaryUrlGenerator ?? $this->adapter;\n\n        if ($generator instanceof TemporaryUrlGenerator) {\n            return $generator->temporaryUrl(\n                $this->pathNormalizer->normalizePath($path),\n                $expiresAt,\n                $this->config->extend($config)\n            );\n        }\n\n        throw UnableToGenerateTemporaryUrl::noGeneratorConfigured($path);\n    }\n\n    public function checksum(string $path, array $config = []): string\n    {\n        $config = $this->config->extend($config);\n\n        if ( ! $this->adapter instanceof ChecksumProvider) {\n            return $this->calculateChecksumFromStream($path, $config);\n        }\n\n        try {\n            return $this->adapter->checksum(\n                $this->pathNormalizer->normalizePath($path),\n                $config,\n            );\n        } catch (ChecksumAlgoIsNotSupported) {\n            return $this->calculateChecksumFromStream(\n                $this->pathNormalizer->normalizePath($path),\n                $config,\n            );\n        }\n    }\n\n    private function resolvePublicUrlGenerator(): ?PublicUrlGenerator\n    {\n        if ($publicUrl = $this->config->get('public_url')) {\n            return match (true) {\n                is_array($publicUrl) => new ShardedPrefixPublicUrlGenerator($publicUrl),\n                default => new PrefixPublicUrlGenerator($publicUrl),\n            };\n        }\n\n        if ($this->adapter instanceof PublicUrlGenerator) {\n            return $this->adapter;\n        }\n\n        return null;\n    }\n\n    /**\n     * @param mixed $contents\n     */\n    private function assertIsResource($contents): void\n    {\n        if (is_resource($contents) === false) {\n            throw new InvalidStreamProvided(\n                \"Invalid stream provided, expected stream resource, received \" . gettype($contents)\n            );\n        } elseif ($type = get_resource_type($contents) !== 'stream') {\n            throw new InvalidStreamProvided(\n                \"Invalid stream provided, expected stream resource, received resource of type \" . $type\n            );\n        }\n    }\n\n    /**\n     * @param resource $resource\n     */\n    private function rewindStream($resource): void\n    {\n        if (ftell($resource) !== 0 && stream_get_meta_data($resource)['seekable']) {\n            rewind($resource);\n        }\n    }\n\n    private function resolveConfigForMoveAndCopy(array $config): Config\n    {\n        $retainVisibility = $this->config->get(Config::OPTION_RETAIN_VISIBILITY, $config[Config::OPTION_RETAIN_VISIBILITY] ?? true);\n        $fullConfig = $this->config->extend($config);\n\n        /*\n         * By default, we retain visibility. When we do not retain visibility, the visibility setting\n         * from the default configuration is ignored. Only when it is set explicitly, we propagate the\n         * setting.\n         */\n        if ($retainVisibility && ! array_key_exists(Config::OPTION_VISIBILITY, $config)) {\n            $fullConfig = $fullConfig->withoutSettings(Config::OPTION_VISIBILITY)->extend($config);\n        }\n\n        return $fullConfig;\n    }\n}\n"
  },
  {
    "path": "src/FilesystemAdapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\ninterface FilesystemAdapter\n{\n    /**\n     * @throws FilesystemException\n     * @throws UnableToCheckExistence\n     */\n    public function fileExists(string $path): bool;\n\n    /**\n     * @throws FilesystemException\n     * @throws UnableToCheckExistence\n     */\n    public function directoryExists(string $path): bool;\n\n    /**\n     * @throws UnableToWriteFile\n     * @throws FilesystemException\n     */\n    public function write(string $path, string $contents, Config $config): void;\n\n    /**\n     * @param resource $contents\n     *\n     * @throws UnableToWriteFile\n     * @throws FilesystemException\n     */\n    public function writeStream(string $path, $contents, Config $config): void;\n\n    /**\n     * @throws UnableToReadFile\n     * @throws FilesystemException\n     */\n    public function read(string $path): string;\n\n    /**\n     * @return resource\n     *\n     * @throws UnableToReadFile\n     * @throws FilesystemException\n     */\n    public function readStream(string $path);\n\n    /**\n     * @throws UnableToDeleteFile\n     * @throws FilesystemException\n     */\n    public function delete(string $path): void;\n\n    /**\n     * @throws UnableToDeleteDirectory\n     * @throws FilesystemException\n     */\n    public function deleteDirectory(string $path): void;\n\n    /**\n     * @throws UnableToCreateDirectory\n     * @throws FilesystemException\n     */\n    public function createDirectory(string $path, Config $config): void;\n\n    /**\n     * @throws InvalidVisibilityProvided\n     * @throws FilesystemException\n     */\n    public function setVisibility(string $path, string $visibility): void;\n\n    /**\n     * @throws UnableToRetrieveMetadata\n     * @throws FilesystemException\n     */\n    public function visibility(string $path): FileAttributes;\n\n    /**\n     * @throws UnableToRetrieveMetadata\n     * @throws FilesystemException\n     */\n    public function mimeType(string $path): FileAttributes;\n\n    /**\n     * @throws UnableToRetrieveMetadata\n     * @throws FilesystemException\n     */\n    public function lastModified(string $path): FileAttributes;\n\n    /**\n     * @throws UnableToRetrieveMetadata\n     * @throws FilesystemException\n     */\n    public function fileSize(string $path): FileAttributes;\n\n    /**\n     * @return iterable<StorageAttributes>\n     *\n     * @throws FilesystemException\n     */\n    public function listContents(string $path, bool $deep): iterable;\n\n    /**\n     * @throws UnableToMoveFile\n     * @throws FilesystemException\n     */\n    public function move(string $source, string $destination, Config $config): void;\n\n    /**\n     * @throws UnableToCopyFile\n     * @throws FilesystemException\n     */\n    public function copy(string $source, string $destination, Config $config): void;\n}\n"
  },
  {
    "path": "src/FilesystemException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse Throwable;\n\ninterface FilesystemException extends Throwable\n{\n}\n"
  },
  {
    "path": "src/FilesystemOperationFailed.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\ninterface FilesystemOperationFailed extends FilesystemException\n{\n    public const OPERATION_WRITE = 'WRITE';\n    public const OPERATION_UPDATE = 'UPDATE'; // not used\n    public const OPERATION_EXISTENCE_CHECK = 'EXISTENCE_CHECK';\n    public const OPERATION_DIRECTORY_EXISTS = 'DIRECTORY_EXISTS';\n    public const OPERATION_FILE_EXISTS = 'FILE_EXISTS';\n    public const OPERATION_CREATE_DIRECTORY = 'CREATE_DIRECTORY';\n    public const OPERATION_DELETE = 'DELETE';\n    public const OPERATION_DELETE_DIRECTORY = 'DELETE_DIRECTORY';\n    public const OPERATION_MOVE = 'MOVE';\n    public const OPERATION_RETRIEVE_METADATA = 'RETRIEVE_METADATA';\n    public const OPERATION_COPY = 'COPY';\n    public const OPERATION_READ = 'READ';\n    public const OPERATION_SET_VISIBILITY = 'SET_VISIBILITY';\n    public const OPERATION_LIST_CONTENTS = 'LIST_CONTENTS';\n\n    public function operation(): string;\n}\n"
  },
  {
    "path": "src/FilesystemOperator.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\ninterface FilesystemOperator extends FilesystemReader, FilesystemWriter\n{\n}\n"
  },
  {
    "path": "src/FilesystemReader.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse DateTimeInterface;\n\n/**\n * This interface contains everything to read from and inspect\n * a filesystem. All methods containing are non-destructive.\n *\n * @method string publicUrl(string $path, array $config = []) Will be added in 4.0\n * @method string temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []) Will be added in 4.0\n * @method string checksum(string $path, array $config = []) Will be added in 4.0\n */\ninterface FilesystemReader\n{\n    public const LIST_SHALLOW = false;\n    public const LIST_DEEP = true;\n\n    /**\n     * @throws FilesystemException\n     * @throws UnableToCheckExistence\n     */\n    public function fileExists(string $location): bool;\n\n    /**\n     * @throws FilesystemException\n     * @throws UnableToCheckExistence\n     */\n    public function directoryExists(string $location): bool;\n\n    /**\n     * @throws FilesystemException\n     * @throws UnableToCheckExistence\n     */\n    public function has(string $location): bool;\n\n    /**\n     * @throws UnableToReadFile\n     * @throws FilesystemException\n     */\n    public function read(string $location): string;\n\n    /**\n     * @return resource\n     *\n     * @throws UnableToReadFile\n     * @throws FilesystemException\n     */\n    public function readStream(string $location);\n\n    /**\n     * @return DirectoryListing<StorageAttributes>\n     *\n     * @throws FilesystemException\n     * @throws UnableToListContents\n     */\n    public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing;\n\n    /**\n     * @throws UnableToRetrieveMetadata\n     * @throws FilesystemException\n     */\n    public function lastModified(string $path): int;\n\n    /**\n     * @throws UnableToRetrieveMetadata\n     * @throws FilesystemException\n     */\n    public function fileSize(string $path): int;\n\n    /**\n     * @throws UnableToRetrieveMetadata\n     * @throws FilesystemException\n     */\n    public function mimeType(string $path): string;\n\n    /**\n     * @throws UnableToRetrieveMetadata\n     * @throws FilesystemException\n     */\n    public function visibility(string $path): string;\n}\n"
  },
  {
    "path": "src/FilesystemTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse DateTimeImmutable;\nuse DateTimeInterface;\nuse Generator;\nuse GuzzleHttp\\Psr7\\StreamWrapper;\nuse GuzzleHttp\\Psr7\\Utils;\nuse IteratorAggregate;\nuse League\\Flysystem\\InMemory\\InMemoryFilesystemAdapter;\nuse League\\Flysystem\\Local\\LocalFilesystemAdapter;\nuse League\\Flysystem\\UrlGeneration\\PublicUrlGenerator;\nuse League\\Flysystem\\UrlGeneration\\TemporaryUrlGenerator;\nuse LogicException;\nuse PHPUnit\\Framework\\TestCase;\n\nuse function iterator_to_array;\n\n/**\n * @group core\n */\nclass FilesystemTest extends TestCase\n{\n    const ROOT = __DIR__ . '/../test_files/test-root';\n\n    /**\n     * @var Filesystem\n     */\n    private $filesystem;\n\n    /**\n     * @before\n     */\n    public function setupFilesystem(): void\n    {\n        $adapter = new LocalFilesystemAdapter(self::ROOT);\n        $filesystem = new Filesystem($adapter);\n        $this->filesystem = $filesystem;\n    }\n\n    /**\n     * @after\n     */\n    public function removeFiles(): void\n    {\n        delete_directory(static::ROOT);\n    }\n\n    /**\n     * @test\n     */\n    public function writing_and_reading_files(): void\n    {\n        $this->filesystem->write('path.txt', 'contents');\n        $contents = $this->filesystem->read('path.txt');\n\n        $this->assertEquals('contents', $contents);\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider invalidStreamInput\n     *\n     * @param mixed $input\n     */\n    public function trying_to_write_with_an_invalid_stream_arguments($input): void\n    {\n        $this->expectException(InvalidStreamProvided::class);\n\n        $this->filesystem->writeStream('path.txt', $input);\n    }\n\n    public static function invalidStreamInput(): Generator\n    {\n        $handle = tmpfile();\n        fclose($handle);\n        yield \"resource that is not open\" => [$handle];\n        yield \"something that is not a resource\" => [false];\n    }\n\n    /**\n     * @test\n     */\n    public function writing_and_reading_a_stream(): void\n    {\n        $writeStream = stream_with_contents('contents');\n\n        $this->filesystem->writeStream('path.txt', $writeStream);\n        $readStream = $this->filesystem->readStream('path.txt');\n\n        fclose($writeStream);\n\n        $this->assertIsResource($readStream);\n        $this->assertEquals('contents', stream_get_contents($readStream));\n\n        fclose($readStream);\n    }\n\n    /**\n     * @test\n     */\n    public function writing_using_a_stream_wrapper(): void\n    {\n        $contents = 'contents of the file';\n        $stream = Utils::streamFor($contents);\n        $resource = StreamWrapper::getResource($stream);\n\n        $this->filesystem->writeStream('from-stream-wrapper.txt', $resource);\n        fclose($resource);\n\n        $this->assertEquals($contents, $this->filesystem->read('from-stream-wrapper.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function checking_if_files_exist(): void\n    {\n        $this->filesystem->write('path.txt', 'contents');\n\n        $pathDotTxtExists = $this->filesystem->fileExists('path.txt');\n        $otherFileExists = $this->filesystem->fileExists('other.txt');\n\n        $this->assertTrue($pathDotTxtExists);\n        $this->assertFalse($otherFileExists);\n    }\n\n    /**\n     * @test\n     */\n    public function checking_if_directories_exist(): void\n    {\n        $this->filesystem->createDirectory('existing-directory');\n\n        $existingDirectory = $this->filesystem->directoryExists('existing-directory');\n        $notExistingDirectory = $this->filesystem->directoryExists('not-existing-directory');\n\n        $this->assertTrue($existingDirectory);\n        $this->assertFalse($notExistingDirectory);\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_file(): void\n    {\n        $this->filesystem->write('path.txt', 'content');\n        $this->filesystem->delete('path.txt');\n\n        $this->assertFalse($this->filesystem->fileExists('path.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function creating_a_directory(): void\n    {\n        $this->filesystem->createDirectory('here');\n\n        $directoryAttrs = $this->filesystem->listContents('')->toArray()[0];\n        $this->assertInstanceOf(DirectoryAttributes::class, $directoryAttrs);\n        $this->assertEquals('here', $directoryAttrs->path());\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_directory(): void\n    {\n        $this->filesystem->write('dirname/a.txt', 'contents');\n        $this->filesystem->write('dirname/b.txt', 'contents');\n        $this->filesystem->write('dirname/c.txt', 'contents');\n\n        $this->filesystem->deleteDirectory('dir');\n\n        $this->assertTrue($this->filesystem->fileExists('dirname/a.txt'));\n\n        $this->filesystem->deleteDirectory('dirname');\n\n        $this->assertFalse($this->filesystem->fileExists('dirname/a.txt'));\n        $this->assertFalse($this->filesystem->fileExists('dirname/b.txt'));\n        $this->assertFalse($this->filesystem->fileExists('dirname/c.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function listing_directory_contents(): void\n    {\n        $this->filesystem->write('dirname/a.txt', 'contents');\n        $this->filesystem->write('dirname/b.txt', 'contents');\n        $this->filesystem->write('dirname/c.txt', 'contents');\n\n        $listing = $this->filesystem->listContents('', false);\n\n        $this->assertInstanceOf(DirectoryListing::class, $listing);\n        $this->assertInstanceOf(IteratorAggregate::class, $listing);\n\n        $attributeListing = iterator_to_array($listing);\n        $this->assertContainsOnlyInstancesOf(StorageAttributes::class, $attributeListing);\n        $this->assertCount(1, $attributeListing);\n    }\n\n    /**\n     * @test\n     */\n    public function listing_directory_contents_recursive(): void\n    {\n        $this->filesystem->write('dirname/a.txt', 'contents');\n        $this->filesystem->write('dirname/b.txt', 'contents');\n        $this->filesystem->write('dirname/c.txt', 'contents');\n\n        $listing = $this->filesystem->listContents('', true);\n\n        $attributeListing = $listing->toArray();\n        $this->assertContainsOnlyInstancesOf(StorageAttributes::class, $attributeListing);\n        $this->assertCount(4, $attributeListing);\n    }\n\n    /**\n     * @test\n     */\n    public function copying_files(): void\n    {\n        $this->filesystem->write('path.txt', 'contents');\n\n        $this->filesystem->copy('path.txt', 'new-path.txt');\n\n        $this->assertTrue($this->filesystem->fileExists('path.txt'));\n        $this->assertTrue($this->filesystem->fileExists('new-path.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function moving_files(): void\n    {\n        $this->filesystem->write('path.txt', 'contents');\n\n        $this->filesystem->move('path.txt', 'new-path.txt');\n\n        $this->assertFalse($this->filesystem->fileExists('path.txt'));\n        $this->assertTrue($this->filesystem->fileExists('new-path.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_last_modified(): void\n    {\n        $this->filesystem->write('path.txt', 'contents');\n\n        $lastModified = $this->filesystem->lastModified('path.txt');\n\n        $this->assertIsInt($lastModified);\n        $this->assertTrue($lastModified > time() - 30);\n        $this->assertTrue($lastModified < time() + 30);\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_mime_type(): void\n    {\n        $this->filesystem->write('path.txt', 'contents');\n\n        $mimeType = $this->filesystem->mimeType('path.txt');\n\n        $this->assertEquals('text/plain', $mimeType);\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_file_size(): void\n    {\n        $this->filesystem->write('path.txt', 'contents');\n\n        $fileSize = $this->filesystem->fileSize('path.txt');\n\n        $this->assertEquals(8, $fileSize);\n    }\n\n    /**\n     * @test\n     */\n    public function ensuring_streams_are_rewound_when_writing(): void\n    {\n        $writeStream = stream_with_contents('contents');\n        fseek($writeStream, 4);\n\n        $this->filesystem->writeStream('path.txt', $writeStream);\n        $contents = $this->filesystem->read('path.txt');\n\n        $this->assertEquals('contents', $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function setting_visibility(): void\n    {\n        $this->filesystem->write('path.txt', 'contents');\n\n        $this->filesystem->setVisibility('path.txt', Visibility::PUBLIC);\n        $publicVisibility = $this->filesystem->visibility('path.txt');\n\n        $this->filesystem->setVisibility('path.txt', Visibility::PRIVATE);\n        $privateVisibility = $this->filesystem->visibility('path.txt');\n\n        $this->assertEquals(Visibility::PUBLIC, $publicVisibility);\n        $this->assertEquals(Visibility::PRIVATE, $privateVisibility);\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider scenariosCausingPathTraversal\n     */\n    public function protecting_against_path_traversals(callable $scenario): void\n    {\n        $this->expectException(PathTraversalDetected::class);\n        $scenario($this->filesystem);\n    }\n\n    public static function scenariosCausingPathTraversal(): Generator\n    {\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->delete('../path.txt');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->deleteDirectory('../path');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->createDirectory('../path');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->read('../path.txt');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->readStream('../path.txt');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->write('../path.txt', 'contents');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $stream = stream_with_contents('contents');\n            try {\n                $filesystem->writeStream('../path.txt', $stream);\n            } finally {\n                fclose($stream);\n            }\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->listContents('../path');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->fileExists('../path.txt');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->mimeType('../path.txt');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->fileSize('../path.txt');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->lastModified('../path.txt');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->visibility('../path.txt');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->setVisibility('../path.txt', Visibility::PUBLIC);\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->copy('../path.txt', 'path.txt');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->copy('path.txt', '../path.txt');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->move('../path.txt', 'path.txt');\n        }];\n        yield [function (FilesystemOperator $filesystem) {\n            $filesystem->move('path.txt', '../path.txt');\n        }];\n    }\n\n    /**\n     * @test\n     */\n    public function listing_exceptions_are_uniformely_represented(): void\n    {\n        $filesystem = new Filesystem(\n            new class() extends InMemoryFilesystemAdapter {\n                public function listContents(string $path, bool $deep): iterable\n                {\n                    yield from parent::listContents($path, $deep);\n                    throw new LogicException('Oh no.');\n                }\n            }\n        );\n        $items = $filesystem->listContents('', true);\n\n        $this->expectException(UnableToListContents::class);\n\n        iterator_to_array($items); // force the yields\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_create_a_public_url(): void\n    {\n        $filesystem = new Filesystem(\n            new class() extends InMemoryFilesystemAdapter implements PublicUrlGenerator {\n                public function publicUrl(string $path, Config $config): string\n                {\n                    throw new UnableToGeneratePublicUrl('No reason', $path);\n                }\n            }\n        );\n\n        $this->expectException(UnableToGeneratePublicUrl::class);\n\n        $filesystem->publicUrl('path.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function not_configuring_a_public_url(): void\n    {\n        $filesystem = new Filesystem(new InMemoryFilesystemAdapter());\n\n        $this->expectException(UnableToGeneratePublicUrl::class);\n\n        $filesystem->publicUrl('path.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function creating_a_public_url(): void\n    {\n        $filesystem = new Filesystem(\n            new InMemoryFilesystemAdapter(),\n            ['public_url' => 'https://example.org/public/'],\n        );\n\n        $url = $filesystem->publicUrl('path.txt');\n\n        self::assertEquals('https://example.org/public/path.txt', $url);\n    }\n\n    /**\n     * @test\n     */\n    public function public_url_array_uses_multi_prefixer(): void\n    {\n        $filesystem = new Filesystem(\n            new InMemoryFilesystemAdapter(),\n            ['public_url' => ['https://cdn1', 'https://cdn2']],\n        );\n\n        $url1 = $filesystem->publicUrl('first-path1.txt');\n        $url2 = $filesystem->publicUrl('path2.txt');\n        $url3 = $filesystem->publicUrl('first-path1.txt'); // deterministic\n        $url4 = $filesystem->publicUrl('/some/path-here.txt');\n        $url5 = $filesystem->publicUrl('some/path-here.txt'); // deterministic even with leading \"/\"\n\n        self::assertEquals('https://cdn1/first-path1.txt', $url1);\n        self::assertEquals('https://cdn2/path2.txt', $url2);\n        self::assertEquals('https://cdn1/first-path1.txt', $url3);\n        self::assertEquals('https://cdn2/some/path-here.txt', $url4);\n        self::assertEquals('https://cdn2/some/path-here.txt', $url5);\n    }\n\n    /**\n     * @test\n     */\n    public function custom_public_url_generator(): void\n    {\n        $filesystem = new Filesystem(\n            new InMemoryFilesystemAdapter(),\n            [],\n            publicUrlGenerator: new class() implements PublicUrlGenerator {\n                public function publicUrl(string $path, Config $config): string\n                {\n                    return 'custom/' . $path;\n                }\n            },\n        );\n\n        self::assertSame('custom/file.txt', $filesystem->publicUrl('file.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function copying_from_and_to_the_same_location_fails(): void\n    {\n        $this->expectExceptionObject(UnableToCopyFile::sourceAndDestinationAreTheSame('from.txt', 'from.txt'));\n\n        $config = [Config::OPTION_COPY_IDENTICAL_PATH => ResolveIdenticalPathConflict::FAIL];\n        $this->filesystem->copy('from.txt', 'from.txt', $config);\n    }\n\n    /**\n     * @test\n     */\n    public function moving_from_and_to_the_same_location_fails(): void\n    {\n        $this->expectExceptionObject(UnableToMoveFile::fromLocationTo('from.txt', 'from.txt'));\n\n        $config = [Config::OPTION_MOVE_IDENTICAL_PATH => ResolveIdenticalPathConflict::FAIL];\n        $this->filesystem->move('from.txt', 'from.txt', $config);\n    }\n\n    /**\n     * @test\n     */\n    public function get_checksum_for_adapter_that_supports(): void\n    {\n        $this->filesystem->write('path.txt', 'foobar');\n\n        $this->assertSame('3858f62230ac3c915f300c664312c63f', $this->filesystem->checksum('path.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function get_checksum_for_adapter_that_does_not_support(): void\n    {\n        $filesystem = new Filesystem(new InMemoryFilesystemAdapter());\n\n        $filesystem->write('path.txt', 'foobar');\n\n        $this->assertSame('3858f62230ac3c915f300c664312c63f', $filesystem->checksum('path.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function get_checksum_for_adapter_that_does_not_support_specific_algo(): void\n    {\n        $adapter = new class() extends InMemoryFilesystemAdapter implements ChecksumProvider {\n            public function checksum(string $path, Config $config): string\n            {\n                throw new ChecksumAlgoIsNotSupported();\n            }\n        };\n        $filesystem = new Filesystem($adapter);\n\n        $filesystem->write('path.txt', 'foobar');\n\n        $this->assertSame('3858f62230ac3c915f300c664312c63f', $filesystem->checksum('path.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function get_sha256_checksum_for_adapter_that_does_not_support(): void\n    {\n        $filesystem = new Filesystem(new InMemoryFilesystemAdapter(), ['checksum_algo' => 'sha256']);\n\n        $filesystem->write('path.txt', 'foobar');\n\n        $this->assertSame('c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2', $filesystem->checksum('path.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function get_sha256_checksum_for_adapter_that_does_not_support_while_crc32c_is_the_default(): void\n    {\n        $filesystem = new Filesystem(new InMemoryFilesystemAdapter(), ['checksum_algo' => 'crc32c']);\n\n        $filesystem->write('path.txt', 'foobar');\n        $checksum = $filesystem->checksum('path.txt', ['checksum_algo' => 'sha256']);\n\n        $this->assertSame('c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2', $checksum);\n    }\n\n    /**\n     * @test\n     */\n    public function unable_to_get_checksum_for_for_file_that_does_not_exist(): void\n    {\n        $filesystem = new Filesystem(new InMemoryFilesystemAdapter());\n\n        $this->expectException(UnableToProvideChecksum::class);\n\n        $filesystem->checksum('path.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function generating_temporary_urls(): void\n    {\n        $filesystem = new Filesystem(\n            new InMemoryFilesystemAdapter(),\n            temporaryUrlGenerator: new class() implements TemporaryUrlGenerator {\n                public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string\n                {\n                    return 'https://flysystem.thephpleague.com/' . $path . '?exporesAt=' . $expiresAt->format('U');\n                }\n            }\n        );\n\n        $now = \\time();\n        $temporaryUrl = $filesystem->temporaryUrl('some/file.txt', new DateTimeImmutable('@' . $now));\n        $expectedUrl = 'https://flysystem.thephpleague.com/some/file.txt?exporesAt=' . $now;\n\n        self::assertEquals($expectedUrl, $temporaryUrl);\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_generate_temporary_urls(): void\n    {\n        $filesystem = new Filesystem(new InMemoryFilesystemAdapter());\n\n        $this->expectException(UnableToGenerateTemporaryUrl::class);\n\n        $filesystem->temporaryUrl('some/file.txt', new DateTimeImmutable());\n    }\n\n    /**\n     * @test\n     */\n    public function ignoring_same_paths_for_move_and_copy(): void\n    {\n        $this->expectNotToPerformAssertions();\n\n        $filesystem = new Filesystem(\n            new InMemoryFilesystemAdapter(),\n            [\n                Config::OPTION_COPY_IDENTICAL_PATH => ResolveIdenticalPathConflict::IGNORE,\n                Config::OPTION_MOVE_IDENTICAL_PATH => ResolveIdenticalPathConflict::IGNORE,\n            ]\n        );\n\n        $filesystem->move('from.txt', 'from.txt');\n        $filesystem->copy('from.txt', 'from.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function failing_same_paths_for_move(): void\n    {\n        $filesystem = new Filesystem(\n            new InMemoryFilesystemAdapter(),\n            [\n                Config::OPTION_MOVE_IDENTICAL_PATH => ResolveIdenticalPathConflict::FAIL,\n            ]\n        );\n\n        $this->expectExceptionObject(UnableToMoveFile::fromLocationTo('from.txt', 'from.txt'));\n        $filesystem->move('from.txt', 'from.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function failing_same_paths_for_copy(): void\n    {\n        $filesystem = new Filesystem(\n            new InMemoryFilesystemAdapter(),\n            [\n                Config::OPTION_COPY_IDENTICAL_PATH => ResolveIdenticalPathConflict::FAIL,\n            ]\n        );\n\n        $this->expectExceptionObject(UnableToCopyFile::fromLocationTo('from.txt', 'from.txt'));\n        $filesystem->copy('from.txt', 'from.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function unable_to_get_checksum_directory(): void\n    {\n        $filesystem = new Filesystem(new InMemoryFilesystemAdapter());\n        $filesystem->createDirectory('foo');\n\n        $this->expectException(UnableToProvideChecksum::class);\n\n        $filesystem->checksum('foo');\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider fileMoveOrCopyScenarios\n     */\n    public function moving_a_file_with_visibility_scenario(\n        array $mainConfig,\n        array $moveConfig,\n        ?string $writeVisibility,\n        string $expectedVisibility\n    ): void {\n        // arrange\n        $filesystem = new Filesystem(\n            new InMemoryFilesystemAdapter(),\n            $mainConfig\n        );\n        $writeConfig = $writeVisibility ? ['visibility' => $writeVisibility] : [];\n        $filesystem->write('from.txt', 'contents', $writeConfig);\n\n        // act\n        $filesystem->move('from.txt', 'to.txt', $moveConfig);\n\n        // assert\n        $this->assertEquals($expectedVisibility, $filesystem->visibility('to.txt'));\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider fileMoveOrCopyScenarios\n     */\n    public function copying_a_file_with_visibility_scenario(\n        array $mainConfig,\n        array $copyConfig,\n        ?string $writeVisibility,\n        string $expectedVisibility\n    ): void {\n        // arrange\n        $filesystem = new Filesystem(\n            new InMemoryFilesystemAdapter(),\n            $mainConfig\n        );\n        $writeConfig = $writeVisibility ? ['visibility' => $writeVisibility] : [];\n        $filesystem->write('from.txt', 'contents', $writeConfig);\n\n        // act\n        $filesystem->copy('from.txt', 'to.txt', $copyConfig);\n\n        // assert\n        $this->assertEquals($expectedVisibility, $filesystem->visibility('to.txt'));\n    }\n\n    public static function fileMoveOrCopyScenarios(): iterable\n    {\n        yield 'retain visibility, write default, default private' => [\n            ['retain_visibility' => true, 'visibility' => 'private'],\n            [],\n            null,\n            'private'\n        ];\n        yield 'retain visibility, write default, default public' => [\n            ['retain_visibility' => true, 'visibility' => 'public'],\n            [],\n            null,\n            'public'\n        ];\n        yield 'retain visibility, write public, default private' => [\n            ['retain_visibility' => true, 'visibility' => 'private'],\n            [],\n            'public',\n            'public'\n        ];\n        yield 'retain visibility, write private, default public' => [\n            ['retain_visibility' => true, 'visibility' => 'public'],\n            [],\n            'private',\n            'private'\n        ];\n\n        yield 'retain visibility, write default, default private, execute public' => [\n            ['retain_visibility' => true, 'visibility' => 'private'],\n            ['visibility' => 'public'],\n            null,\n            'public'\n        ];\n        yield 'retain visibility, write default, default public, execute private' => [\n            ['retain_visibility' => true, 'visibility' => 'public'],\n            ['visibility' => 'private'],\n            null,\n            'private'\n        ];\n        yield 'retain visibility, write public, default private, execute private' => [\n            ['retain_visibility' => true, 'visibility' => 'private'],\n            ['visibility' => 'private'],\n            'public',\n            'private'\n        ];\n        yield 'retain visibility, write private, default public, execute public' => [\n            ['retain_visibility' => true, 'visibility' => 'public'],\n            ['visibility' => 'public'],\n            'private',\n            'public'\n        ];\n\n        yield 'do not retain visibility, write default, default private' => [\n            ['retain_visibility' => false, 'visibility' => 'private'],\n            [],\n            null,\n            'private'\n        ];\n        yield 'do not retain visibility, write default, default public' => [\n            ['retain_visibility' => false, 'visibility' => 'public'],\n            [],\n            null,\n            'public'\n        ];\n        yield 'do not retain visibility, write public, default private' => [\n            ['retain_visibility' => false, 'visibility' => 'private'],\n            [],\n            'public',\n            'private'\n        ];\n        yield 'do not retain visibility, write private, default public' => [\n            ['retain_visibility' => false, 'visibility' => 'public'],\n            [],\n            'private',\n            'public'\n        ];\n\n        yield 'do not retain visibility, write default, default private, execute public' => [\n            ['retain_visibility' => false, 'visibility' => 'private'],\n            ['visibility' => 'public'],\n            null,\n            'public'\n        ];\n        yield 'do not retain visibility, write default, default public, execute private' => [\n            ['retain_visibility' => false, 'visibility' => 'public'],\n            ['visibility' => 'private'],\n            null,\n            'private'\n        ];\n        yield 'do not retain visibility, write public, default private, execute public' => [\n            ['retain_visibility' => false, 'visibility' => 'private'],\n            ['visibility' => 'public'],\n            'public',\n            'public'\n        ];\n        yield 'do not retain visibility, write private, default public, execute private' => [\n            ['retain_visibility' => false, 'visibility' => 'public'],\n            ['visibility' => 'private'],\n            'private',\n            'private'\n        ];\n    }\n}\n"
  },
  {
    "path": "src/FilesystemWriter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\ninterface FilesystemWriter\n{\n    /**\n     * @throws UnableToWriteFile\n     * @throws FilesystemException\n     */\n    public function write(string $location, string $contents, array $config = []): void;\n\n    /**\n     * @param mixed $contents\n     *\n     * @throws UnableToWriteFile\n     * @throws FilesystemException\n     */\n    public function writeStream(string $location, $contents, array $config = []): void;\n\n    /**\n     * @throws UnableToSetVisibility\n     * @throws FilesystemException\n     */\n    public function setVisibility(string $path, string $visibility): void;\n\n    /**\n     * @throws UnableToDeleteFile\n     * @throws FilesystemException\n     */\n    public function delete(string $location): void;\n\n    /**\n     * @throws UnableToDeleteDirectory\n     * @throws FilesystemException\n     */\n    public function deleteDirectory(string $location): void;\n\n    /**\n     * @throws UnableToCreateDirectory\n     * @throws FilesystemException\n     */\n    public function createDirectory(string $location, array $config = []): void;\n\n    /**\n     * @throws UnableToMoveFile\n     * @throws FilesystemException\n     */\n    public function move(string $source, string $destination, array $config = []): void;\n\n    /**\n     * @throws UnableToCopyFile\n     * @throws FilesystemException\n     */\n    public function copy(string $source, string $destination, array $config = []): void;\n}\n"
  },
  {
    "path": "src/Ftp/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n**/*Test.php export-ignore\n**/*TestCase.php export-ignore\nREADME.md export-ignore\n"
  },
  {
    "path": "src/Ftp/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/Ftp/ConnectionProvider.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\ninterface ConnectionProvider\n{\n    /**\n     * @return resource\n     */\n    public function createConnection(FtpConnectionOptions $options);\n}\n"
  },
  {
    "path": "src/Ftp/ConnectivityChecker.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\ninterface ConnectivityChecker\n{\n    /**\n     * @param resource $connection\n     */\n    public function isConnected($connection): bool;\n}\n"
  },
  {
    "path": "src/Ftp/ConnectivityCheckerThatCanFail.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nclass ConnectivityCheckerThatCanFail implements ConnectivityChecker\n{\n    private bool $failNextCall = false;\n\n    public function __construct(private ConnectivityChecker $connectivityChecker)\n    {\n    }\n\n    public function failNextCall(): void\n    {\n        $this->failNextCall = true;\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function isConnected($connection): bool\n    {\n        if ($this->failNextCall) {\n            $this->failNextCall = false;\n\n            return false;\n        }\n\n        return $this->connectivityChecker->isConnected($connection);\n    }\n}\n"
  },
  {
    "path": "src/Ftp/FtpAdapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse DateTime;\nuse Generator;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\StorageAttributes;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UnixVisibility\\PortableVisibilityConverter;\nuse League\\Flysystem\\UnixVisibility\\VisibilityConverter;\nuse League\\MimeTypeDetection\\FinfoMimeTypeDetector;\nuse League\\MimeTypeDetection\\MimeTypeDetector;\nuse Throwable;\n\nuse function array_map;\nuse function error_clear_last;\nuse function error_get_last;\nuse function ftp_chdir;\nuse function ftp_close;\nuse function is_string;\n\nclass FtpAdapter implements FilesystemAdapter\n{\n    private const SYSTEM_TYPE_WINDOWS = 'windows';\n    private const SYSTEM_TYPE_UNIX = 'unix';\n\n    private ConnectionProvider $connectionProvider;\n    private ConnectivityChecker $connectivityChecker;\n\n    /**\n     * @var resource|false|\\FTP\\Connection\n     */\n    private mixed $connection = false;\n    private PathPrefixer $prefixer;\n    private VisibilityConverter $visibilityConverter;\n    private ?bool $isPureFtpdServer = null;\n    private ?bool $useRawListOptions;\n    private ?string $systemType;\n    private MimeTypeDetector $mimeTypeDetector;\n\n    private ?string $rootDirectory = null;\n\n    public function __construct(\n        private FtpConnectionOptions $connectionOptions,\n        ?ConnectionProvider $connectionProvider = null,\n        ?ConnectivityChecker $connectivityChecker = null,\n        ?VisibilityConverter $visibilityConverter = null,\n        ?MimeTypeDetector $mimeTypeDetector = null,\n        private bool $detectMimeTypeUsingPath = false,\n    ) {\n        $this->systemType = $this->connectionOptions->systemType();\n        $this->connectionProvider = $connectionProvider ?? new FtpConnectionProvider();\n        $this->connectivityChecker = $connectivityChecker ?? new NoopCommandConnectivityChecker();\n        $this->visibilityConverter = $visibilityConverter ?? new PortableVisibilityConverter();\n        $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();\n        $this->useRawListOptions = $connectionOptions->useRawListOptions();\n    }\n\n    /**\n     * Disconnect FTP connection on destruct.\n     */\n    public function __destruct()\n    {\n        $this->disconnect();\n    }\n\n    /**\n     * @return resource\n     */\n    private function connection()\n    {\n        start:\n        if ( ! $this->hasFtpConnection()) {\n            $this->connection = $this->connectionProvider->createConnection($this->connectionOptions);\n            $this->rootDirectory = $this->resolveConnectionRoot($this->connection);\n            $this->prefixer = new PathPrefixer($this->rootDirectory);\n\n            return $this->connection;\n        }\n\n        if ($this->connectivityChecker->isConnected($this->connection) === false) {\n            $this->connection = false;\n            goto start;\n        }\n\n        ftp_chdir($this->connection, $this->rootDirectory);\n\n        return $this->connection;\n    }\n\n    public function disconnect(): void\n    {\n        if ($this->hasFtpConnection()) {\n            @ftp_close($this->connection);\n        }\n        $this->connection = false;\n    }\n\n    private function isPureFtpdServer(): bool\n    {\n        if ($this->isPureFtpdServer !== null) {\n            return $this->isPureFtpdServer;\n        }\n\n        $response = ftp_raw($this->connection, 'HELP');\n\n        return $this->isPureFtpdServer = stripos(implode(' ', $response), 'Pure-FTPd') !== false;\n    }\n\n    private function isServerSupportingListOptions(): bool\n    {\n        if ($this->useRawListOptions !== null) {\n            return $this->useRawListOptions;\n        }\n\n        $response = ftp_raw($this->connection, 'SYST');\n        $syst = implode(' ', $response);\n\n        return $this->useRawListOptions = stripos($syst, 'FileZilla') === false\n            && stripos($syst, 'L8') === false;\n    }\n\n    public function fileExists(string $path): bool\n    {\n        try {\n            $this->fileSize($path);\n\n            return true;\n        } catch (UnableToRetrieveMetadata $exception) {\n            return false;\n        }\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        try {\n            $writeStream = fopen('php://temp', 'w+b');\n            fwrite($writeStream, $contents);\n            rewind($writeStream);\n            $this->writeStream($path, $writeStream, $config);\n        } finally {\n            isset($writeStream) && is_resource($writeStream) && fclose($writeStream);\n        }\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        try {\n            $this->ensureParentDirectoryExists($path, $config->get(Config::OPTION_DIRECTORY_VISIBILITY));\n        } catch (Throwable $exception) {\n            throw UnableToWriteFile::atLocation($path, 'creating parent directory failed', $exception);\n        }\n\n        $location = $this->prefixer()->prefixPath($path);\n\n        if ( ! ftp_fput($this->connection(), $location, $contents, $this->connectionOptions->transferMode())) {\n            throw UnableToWriteFile::atLocation($path, 'writing the file failed');\n        }\n\n        if ( ! $visibility = $config->get(Config::OPTION_VISIBILITY)) {\n            return;\n        }\n\n        try {\n            $this->setVisibility($path, $visibility);\n        } catch (Throwable $exception) {\n            throw UnableToWriteFile::atLocation($path, 'setting visibility failed', $exception);\n        }\n    }\n\n    public function read(string $path): string\n    {\n        $readStream = $this->readStream($path);\n        $contents = stream_get_contents($readStream);\n        fclose($readStream);\n\n        return $contents;\n    }\n\n    public function readStream(string $path)\n    {\n        $location = $this->prefixer()->prefixPath($path);\n        $stream = fopen('php://temp', 'w+b');\n        $result = @ftp_fget($this->connection(), $stream, $location, $this->connectionOptions->transferMode());\n\n        if ( ! $result) {\n            fclose($stream);\n\n            throw UnableToReadFile::fromLocation($path, error_get_last()['message'] ?? '');\n        }\n\n        rewind($stream);\n\n        return $stream;\n    }\n\n    public function delete(string $path): void\n    {\n        $connection = $this->connection();\n        $this->deleteFile($path, $connection);\n    }\n\n    /**\n     * @param resource $connection\n     */\n    private function deleteFile(string $path, $connection): void\n    {\n        $location = $this->prefixer()->prefixPath($path);\n        $success = @ftp_delete($connection, $location);\n\n        if ($success === false && ftp_size($connection, $location) !== -1) {\n            throw UnableToDeleteFile::atLocation($path, 'the file still exists');\n        }\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        /** @var StorageAttributes[] $contents */\n        $contents = $this->listContents($path, true);\n        $connection = $this->connection();\n        $directories = [$path];\n\n        foreach ($contents as $item) {\n            if ($item->isDir()) {\n                $directories[] = $item->path();\n                continue;\n            }\n            try {\n                $this->deleteFile($item->path(), $connection);\n            } catch (Throwable $exception) {\n                throw UnableToDeleteDirectory::atLocation($path, 'unable to delete child', $exception);\n            }\n        }\n\n        rsort($directories);\n\n        foreach ($directories as $directory) {\n            if ( ! @ftp_rmdir($connection, $this->prefixer()->prefixPath($directory))) {\n                throw UnableToDeleteDirectory::atLocation($path, \"Could not delete directory $directory\");\n            }\n        }\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        $this->ensureDirectoryExists($path, $config->get(Config::OPTION_DIRECTORY_VISIBILITY, $config->get(Config::OPTION_VISIBILITY)));\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        $location = $this->prefixer()->prefixPath($path);\n        $mode = $this->visibilityConverter->forFile($visibility);\n\n        if ( ! @ftp_chmod($this->connection(), $mode, $location)) {\n            $message = error_get_last()['message'] ?? '';\n            throw UnableToSetVisibility::atLocation($path, $message);\n        }\n    }\n\n    private function fetchMetadata(string $path, string $type): FileAttributes\n    {\n        $location = $this->prefixer()->prefixPath($path);\n\n        if ($this->isPureFtpdServer) {\n            $location = $this->escapePath($location);\n        }\n\n        $object = @ftp_raw($this->connection(), 'STAT ' . $location);\n\n        if (empty($object) || count($object) < 3 || str_starts_with($object[1], \"ftpd:\")) {\n            throw UnableToRetrieveMetadata::create($path, $type, error_get_last()['message'] ?? '');\n        }\n\n        $attributes = $this->normalizeObject($object[1], '');\n\n        if ( ! $attributes instanceof FileAttributes) {\n            throw UnableToRetrieveMetadata::create(\n                $path,\n                $type,\n                'expected file, ' . ($attributes instanceof DirectoryAttributes ? 'directory found' : 'nothing found')\n            );\n        }\n\n        return $attributes;\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        try {\n            $mimetype = $this->detectMimeTypeUsingPath\n                ? $this->mimeTypeDetector->detectMimeTypeFromPath($path)\n                : $this->mimeTypeDetector->detectMimeType($path, $this->read($path));\n        } catch (Throwable $exception) {\n            throw UnableToRetrieveMetadata::mimeType($path, $exception->getMessage(), $exception);\n        }\n\n        if ($mimetype === null) {\n            throw UnableToRetrieveMetadata::mimeType($path, 'Unknown.');\n        }\n\n        return new FileAttributes($path, null, null, null, $mimetype);\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        $location = $this->prefixer()->prefixPath($path);\n        $connection = $this->connection();\n        $lastModified = @ftp_mdtm($connection, $location);\n\n        if ($lastModified < 0) {\n            throw UnableToRetrieveMetadata::lastModified($path);\n        }\n\n        return new FileAttributes($path, null, null, $lastModified);\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        return $this->fetchMetadata($path, FileAttributes::ATTRIBUTE_VISIBILITY);\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        $location = $this->prefixer()->prefixPath($path);\n        $connection = $this->connection();\n        $fileSize = @ftp_size($connection, $location);\n\n        if ($fileSize < 0) {\n            throw UnableToRetrieveMetadata::fileSize($path, error_get_last()['message'] ?? '');\n        }\n\n        return new FileAttributes($path, $fileSize);\n    }\n\n    public function listContents(string $path, bool $deep): iterable\n    {\n        $path = ltrim($path, '/');\n        $path = $path === '' ? $path : trim($path, '/') . '/';\n\n        if ($deep && $this->connectionOptions->recurseManually()) {\n            yield from $this->listDirectoryContentsRecursive($path);\n        } else {\n            $location = $this->prefixer()->prefixPath($path);\n            $options = $deep ? '-alnR' : '-aln';\n            $listing = $this->ftpRawlist($options, $location);\n            yield from $this->normalizeListing($listing, $path);\n        }\n    }\n\n    private function normalizeListing(array $listing, string $prefix = ''): Generator\n    {\n        $base = $prefix;\n\n        foreach ($listing as $item) {\n            if ($item === '' || preg_match('#.* \\.(\\.)?$|^total#', $item)) {\n                continue;\n            }\n\n            if (preg_match('#^.*:$#', $item)) {\n                $base = preg_replace('~^\\./*|:$~', '', $item);\n                continue;\n            }\n\n            yield $this->normalizeObject($item, $base);\n        }\n    }\n\n    private function normalizeObject(string $item, string $base): StorageAttributes\n    {\n        $this->systemType === null && $this->systemType = $this->detectSystemType($item);\n\n        if ($this->systemType === self::SYSTEM_TYPE_UNIX) {\n            return $this->normalizeUnixObject($item, $base);\n        }\n\n        return $this->normalizeWindowsObject($item, $base);\n    }\n\n    private function detectSystemType(string $item): string\n    {\n        return preg_match(\n            '/^[0-9]{2,4}-[0-9]{2}-[0-9]{2}/',\n            $item\n        ) ? self::SYSTEM_TYPE_WINDOWS : self::SYSTEM_TYPE_UNIX;\n    }\n\n    private function normalizeWindowsObject(string $item, string $base): StorageAttributes\n    {\n        $item = preg_replace('#\\s+#', ' ', trim($item), 3);\n        $parts = explode(' ', $item, 4);\n\n        if (count($parts) !== 4) {\n            throw new InvalidListResponseReceived(\"Metadata can't be parsed from item '$item' , not enough parts.\");\n        }\n\n        [$date, $time, $size, $name] = $parts;\n        $path = $base === '' ? $name : rtrim($base, '/') . '/' . $name;\n\n        if ($size === '<DIR>') {\n            return new DirectoryAttributes($path);\n        }\n\n        // Check for the correct date/time format\n        $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i';\n        $dt = DateTime::createFromFormat($format, $date . $time);\n        $lastModified = $dt ? $dt->getTimestamp() : (int) strtotime(\"$date $time\");\n\n        return new FileAttributes($path, (int) $size, null, $lastModified);\n    }\n\n    private function normalizeUnixObject(string $item, string $base): StorageAttributes\n    {\n        $item = preg_replace('#\\s+#', ' ', trim($item), 7);\n        $parts = explode(' ', $item, 9);\n\n        if (count($parts) !== 9) {\n            throw new InvalidListResponseReceived(\"Metadata can't be parsed from item '$item' , not enough parts.\");\n        }\n\n        [$permissions, /* $number */, /* $owner */, /* $group */, $size, $month, $day, $timeOrYear, $name] = $parts;\n        $isDirectory = $this->listingItemIsDirectory($permissions);\n        $permissions = $this->normalizePermissions($permissions);\n        $path = $base === '' ? $name : rtrim($base, '/') . '/' . $name;\n        $lastModified = $this->connectionOptions->timestampsOnUnixListingsEnabled() ? $this->normalizeUnixTimestamp(\n            $month,\n            $day,\n            $timeOrYear\n        ) : null;\n\n        if ($isDirectory) {\n            return new DirectoryAttributes(\n                $path,\n                $this->visibilityConverter->inverseForDirectory($permissions),\n                $lastModified\n            );\n        }\n\n        $visibility = $this->visibilityConverter->inverseForFile($permissions);\n\n        return new FileAttributes($path, (int) $size, $visibility, $lastModified);\n    }\n\n    private function listingItemIsDirectory(string $permissions): bool\n    {\n        return str_starts_with($permissions, 'd');\n    }\n\n    private function normalizeUnixTimestamp(string $month, string $day, string $timeOrYear): int\n    {\n        if (is_numeric($timeOrYear)) {\n            $year = $timeOrYear;\n            $hour = '00';\n            $minute = '00';\n        } else {\n            $year = date('Y');\n            [$hour, $minute] = explode(':', $timeOrYear);\n        }\n\n        $dateTime = DateTime::createFromFormat('Y-M-j-G:i:s', \"$year-$month-$day-$hour:$minute:00\");\n\n        return $dateTime->getTimestamp();\n    }\n\n    private function normalizePermissions(string $permissions): int\n    {\n        // remove the type identifier\n        $permissions = substr($permissions, 1);\n\n        // map the string rights to the numeric counterparts\n        $map = ['-' => '0', 'r' => '4', 'w' => '2', 'x' => '1'];\n        $permissions = strtr($permissions, $map);\n\n        // split up the permission groups\n        $parts = str_split($permissions, 3);\n\n        // convert the groups\n        $mapper = static function ($part) {\n            return array_sum(array_map(static function ($p) {\n                return (int) $p;\n            }, str_split($part)));\n        };\n\n        // converts to decimal number\n        return octdec(implode('', array_map($mapper, $parts)));\n    }\n\n    private function listDirectoryContentsRecursive(string $directory): Generator\n    {\n        $location = $this->prefixer()->prefixPath($directory);\n        $listing = $this->ftpRawlist('-aln', $location);\n        /** @var StorageAttributes[] $listing */\n        $listing = $this->normalizeListing($listing, $directory);\n\n        foreach ($listing as $item) {\n            yield $item;\n\n            if ( ! $item->isDir()) {\n                continue;\n            }\n\n            $children = $this->listDirectoryContentsRecursive($item->path());\n\n            foreach ($children as $child) {\n                yield $child;\n            }\n        }\n    }\n\n    private function ftpRawlist(string $options, string $path): array\n    {\n        $path = rtrim($path, '/') . '/';\n        $connection = $this->connection();\n\n        if ($this->isPureFtpdServer()) {\n            $path = str_replace(' ', '\\ ', $path);\n            $path = $this->escapePath($path);\n        }\n\n        if ( ! $this->isServerSupportingListOptions()) {\n            $options = '';\n        }\n\n        return ftp_rawlist($connection, ($options ? $options . ' ' : '') . $path, stripos($options, 'R') !== false) ?: [];\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        try {\n            $this->ensureParentDirectoryExists($destination, $config->get(Config::OPTION_DIRECTORY_VISIBILITY));\n        } catch (Throwable $exception) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);\n        }\n\n        $sourceLocation = $this->prefixer()->prefixPath($source);\n        $destinationLocation = $this->prefixer()->prefixPath($destination);\n        $connection = $this->connection();\n\n        if ( ! @ftp_rename($connection, $sourceLocation, $destinationLocation)) {\n            throw UnableToMoveFile::because(error_get_last()['message'] ?? 'reason unknown', $source, $destination);\n        }\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        try {\n            $readStream = $this->readStream($source);\n            $visibility = $config->get(Config::OPTION_VISIBILITY);\n\n            if ($visibility === null && $config->get(Config::OPTION_RETAIN_VISIBILITY, true)) {\n                $config = $config->withSetting(Config::OPTION_VISIBILITY, $this->visibility($source)->visibility());\n            }\n\n            $this->writeStream($destination, $readStream, $config);\n        } catch (Throwable $exception) {\n            if (isset($readStream) && is_resource($readStream)) {\n                @fclose($readStream);\n            }\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    private function ensureParentDirectoryExists(string $path, ?string $visibility): void\n    {\n        $dirname = dirname($path);\n\n        if ($dirname === '' || $dirname === '.') {\n            return;\n        }\n\n        $this->ensureDirectoryExists($dirname, $visibility);\n    }\n\n    private function ensureDirectoryExists(string $dirname, ?string $visibility): void\n    {\n        $connection = $this->connection();\n\n        $dirPath = '';\n        $parts = explode('/', trim($dirname, '/'));\n        $mode = $visibility ? $this->visibilityConverter->forDirectory($visibility) : false;\n\n        foreach ($parts as $part) {\n            $dirPath .= '/' . $part;\n            $location = $this->prefixer()->prefixPath($dirPath);\n\n            if (@ftp_chdir($connection, $location)) {\n                continue;\n            }\n\n            error_clear_last();\n            $result = @ftp_mkdir($connection, $location);\n\n            if ($result === false) {\n                $errorMessage = error_get_last()['message'] ?? 'unable to create the directory';\n                throw UnableToCreateDirectory::atLocation($dirPath, $errorMessage);\n            }\n\n            if ($mode !== false && @ftp_chmod($connection, $mode, $location) === false) {\n                throw UnableToCreateDirectory::atLocation(\n                    $dirPath,\n                    'unable to chmod the directory: ' . (error_get_last()['message'] ?? 'reason unknown'),\n                );\n            }\n        }\n    }\n\n    private function escapePath(string $path): string\n    {\n        return str_replace(['*', '[', ']'], ['\\\\*', '\\\\[', '\\\\]'], $path);\n    }\n\n    /**\n     * @return bool\n     */\n    private function hasFtpConnection(): bool\n    {\n        return $this->connection instanceof \\FTP\\Connection || is_resource($this->connection);\n    }\n\n    public function directoryExists(string $path): bool\n    {\n        $location = $this->prefixer()->prefixPath($path);\n        $connection = $this->connection();\n\n        return @ftp_chdir($connection, $location) === true;\n    }\n\n    /**\n     * @param resource|\\FTP\\Connection $connection\n     */\n    private function resolveConnectionRoot($connection): string\n    {\n        $root = $this->connectionOptions->root();\n        error_clear_last();\n\n        if ($root !== '' && @ftp_chdir($connection, $root) !== true) {\n            throw UnableToResolveConnectionRoot::itDoesNotExist($root, error_get_last()['message'] ?? '');\n        }\n\n        error_clear_last();\n        $pwd = @ftp_pwd($connection);\n\n        if ( ! is_string($pwd)) {\n            throw UnableToResolveConnectionRoot::couldNotGetCurrentDirectory(error_get_last()['message'] ?? '');\n        }\n\n        return $pwd;\n    }\n\n    /**\n     * @return PathPrefixer\n     */\n    private function prefixer(): PathPrefixer\n    {\n        if ($this->rootDirectory === null) {\n            $this->connection();\n        }\n\n        return $this->prefixer;\n    }\n}\n"
  },
  {
    "path": "src/Ftp/FtpAdapterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse League\\Flysystem\\FilesystemAdapter;\n\nuse function mock_function;\nuse function reset_function_mocks;\n\n/**\n * @group ftp\n */\nclass FtpAdapterTest extends FtpAdapterTestCase\n{\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        $options = FtpConnectionOptions::fromArray([\n           'host' => 'localhost',\n           'port' => 2121,\n           'timestampsOnUnixListingsEnabled' => true,\n           'root' => '/home/foo/upload/',\n           'username' => 'foo',\n           'password' => 'pass',\n       ]);\n\n        static::$connectivityChecker = new ConnectivityCheckerThatCanFail(new NoopCommandConnectivityChecker());\n        static::$connectionProvider = new StubConnectionProvider(new FtpConnectionProvider());\n\n        return new FtpAdapter(\n            $options,\n            static::$connectionProvider,\n            static::$connectivityChecker,\n        );\n    }\n\n    /**\n     * @test\n     */\n    public function disconnect_after_destruct(): void\n    {\n        /** @var FtpAdapter $adapter */\n        $adapter = $this->adapter();\n        $reflection = new \\ReflectionObject($adapter);\n        $adapter->fileExists('foo.txt');\n        $reflectionProperty = $reflection->getProperty('connection');\n        $reflectionProperty->setAccessible(true);\n        $connection = $reflectionProperty->getValue($adapter);\n        unset($reflection);\n\n        $this->assertTrue(false !== ftp_pwd($connection));\n        $adapter->__destruct();\n        static::clearFilesystemAdapterCache();\n        $this->assertFalse((new NoopCommandConnectivityChecker())->isConnected($connection));\n    }\n\n    /**\n     * @test\n     */\n    public function it_can_disconnect(): void\n    {\n        /** @var FtpAdapter $adapter */\n        $adapter = $this->adapter();\n\n        $this->assertFalse($adapter->fileExists('not-existing.file'));\n\n        self::assertTrue(static::$connectivityChecker->isConnected(static::$connectionProvider->connection));\n        $adapter->disconnect();\n        self::assertFalse(static::$connectivityChecker->isConnected(static::$connectionProvider->connection));\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_resolve_connection_root(): void\n    {\n        $options = FtpConnectionOptions::fromArray([\n           'host' => 'localhost',\n           'port' => 2121,\n           'timestampsOnUnixListingsEnabled' => true,\n           'root' => '/invalid/root',\n           'username' => 'foo',\n           'password' => 'pass',\n        ]);\n\n        $adapter = new FtpAdapter($options);\n\n        $this->expectExceptionObject(UnableToResolveConnectionRoot::itDoesNotExist('/invalid/root'));\n\n        $adapter->delete('something');\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_resolve_connection_root_pwd(): void\n    {\n        $options = FtpConnectionOptions::fromArray([\n           'host' => 'localhost',\n           'port' => 2121,\n           'timestampsOnUnixListingsEnabled' => true,\n           'root' => '/home/foo/upload/',\n           'username' => 'foo',\n           'password' => 'pass',\n        ]);\n\n        $this->expectExceptionObject(UnableToResolveConnectionRoot::couldNotGetCurrentDirectory());\n        mock_function('ftp_pwd', false);\n\n        $adapter = new FtpAdapter($options);\n        $adapter->delete('something');\n    }\n\n    protected function tearDown(): void\n    {\n        reset_function_mocks();\n    }\n}\n"
  },
  {
    "path": "src/Ftp/FtpAdapterTestCase.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse Generator;\nuse League\\Flysystem\\AdapterTestUtilities\\FilesystemAdapterTestCase;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\StorageAttributes;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\Visibility;\n\nuse function iterator_to_array;\n\n/**\n * @group ftp\n *\n * @codeCoverageIgnore\n */\nabstract class FtpAdapterTestCase extends FilesystemAdapterTestCase\n{\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this->retryOnException(UnableToConnectToFtpHost::class);\n    }\n\n    protected static ConnectivityCheckerThatCanFail $connectivityChecker;\n\n    protected static ?StubConnectionProvider $connectionProvider;\n\n    /**\n     * @after\n     */\n    public function resetFunctionMocks(): void\n    {\n        reset_function_mocks();\n    }\n\n    public static function clearFilesystemAdapterCache(): void\n    {\n        parent::clearFilesystemAdapterCache();\n        static::$connectionProvider = null;\n    }\n\n    /**\n     * @test\n     */\n    public function using_empty_string_for_root(): void\n    {\n        $options = FtpConnectionOptions::fromArray([\n            'host' => 'localhost',\n            'port' => 2121,\n            'root' => '',\n            'username' => 'foo',\n            'password' => 'pass',\n        ]);\n\n        $this->runScenario(function () use ($options) {\n            $adapter = new FtpAdapter($options);\n\n            $adapter->write('dirname1/dirname2/path.txt', 'contents', new Config());\n            $adapter->write('dirname1/dirname2/path.txt', 'contents', new Config());\n\n            $this->assertTrue($adapter->fileExists('dirname1/dirname2/path.txt'));\n            $this->assertSame('contents', $adapter->read('dirname1/dirname2/path.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function reconnecting_after_failure(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            static::$connectivityChecker->failNextCall();\n\n            $contents = iterator_to_array($adapter->listContents('', false));\n            $this->assertIsArray($contents);\n        });\n    }\n\n    /**\n     * @test\n     *\n     * @see https://github.com/thephpleague/flysystem/issues/1522\n     */\n    public function reading_a_file_twice_for_issue_1522(): void\n    {\n        $this->givenWeHaveAnExistingFile('some/nested/path.txt', 'this is it');\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n\n            self::assertEquals('this is it', $adapter->read('some/nested/path.txt'));\n            self::assertEquals('this is it', $adapter->read('some/nested/path.txt'));\n            self::assertEquals('this is it', $adapter->read('some/nested/path.txt'));\n        });\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider scenariosCausingWriteFailure\n     */\n    public function failing_to_write_a_file(callable $scenario): void\n    {\n        $this->runScenario(function () use ($scenario) {\n            $scenario();\n        });\n\n        $this->expectException(UnableToWriteFile::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->write('some/path.txt', 'contents', new Config([\n                Config::OPTION_VISIBILITY => Visibility::PUBLIC,\n                Config::OPTION_DIRECTORY_VISIBILITY => Visibility::PUBLIC\n            ]));\n        });\n    }\n\n    public static function scenariosCausingWriteFailure(): Generator\n    {\n        yield \"Not being able to create the parent directory\" => [function () {\n            mock_function('ftp_mkdir', false);\n        }];\n\n        yield \"Not being able to set the parent directory visibility\" => [function () {\n            mock_function('ftp_chmod', false);\n        }];\n\n        yield \"Not being able to write the file\" => [function () {\n            mock_function('ftp_fput', false);\n        }];\n\n        yield \"Not being able to set the visibility\" => [function () {\n            mock_function('ftp_chmod', true, false);\n        }];\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider scenariosCausingDirectoryDeleteFailure\n     */\n    public function scenarios_causing_directory_deletion_to_fail(callable $scenario): void\n    {\n        $this->runScenario($scenario);\n        $this->givenWeHaveAnExistingFile('some/nested/path.txt');\n\n        $this->expectException(UnableToDeleteDirectory::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->deleteDirectory('some');\n        });\n    }\n\n    public static function scenariosCausingDirectoryDeleteFailure(): Generator\n    {\n        yield \"ftp_delete failure\" => [function () {\n            mock_function('ftp_delete', false);\n        }];\n\n        yield \"ftp_rmdir failure\" => [function () {\n            mock_function('ftp_rmdir', false);\n        }];\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider scenariosCausingCopyFailure\n     */\n    public function failing_to_copy(callable $scenario): void\n    {\n        $this->givenWeHaveAnExistingFile('path.txt');\n        $scenario();\n\n        $this->expectException(UnableToCopyFile::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->copy('path.txt', 'new/path.txt', new Config());\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_move_because_creating_the_directory_fails(): void\n    {\n        $this->givenWeHaveAnExistingFile('path.txt');\n        mock_function('ftp_mkdir', false);\n\n        $this->expectException(UnableToMoveFile::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->move('path.txt', 'new/path.txt', new Config());\n        });\n    }\n\n    public static function scenariosCausingCopyFailure(): Generator\n    {\n        yield \"failing to read\" => [function () {\n            mock_function('ftp_fget', false);\n        }];\n\n        yield \"failing to write\" => [function () {\n            mock_function('ftp_fput', false);\n        }];\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_delete_a_file(): void\n    {\n        $this->givenWeHaveAnExistingFile('path.txt', 'contents');\n        mock_function('ftp_delete', false);\n\n        $this->expectException(UnableToDeleteFile::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->delete('path.txt');\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function formatting_a_directory_listing_with_a_total_indicator(): void\n    {\n        $response = [\n            'total 1',\n            '-rw-r--r--   1 ftp      ftp           409 Aug 19 09:01 file1.txt',\n        ];\n        mock_function('ftp_rawlist', $response);\n\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $contents = iterator_to_array($adapter->listContents('/', false), false);\n\n            $this->assertCount(1, $contents);\n            $this->assertContainsOnlyInstancesOf(FileAttributes::class, $contents);\n        });\n    }\n\n    /**\n     * @test\n     *\n     * @runInSeparateProcess\n     */\n    public function receiving_a_windows_listing(): void\n    {\n        $response = [\n            '2015-05-23  12:09       <DIR>          dir1',\n            '05-23-15  12:09PM                  684 file2.txt',\n        ];\n        mock_function('ftp_rawlist', $response);\n\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $contents = iterator_to_array($adapter->listContents('/', false), false);\n\n            $this->assertCount(2, $contents);\n            $this->assertContainsOnlyInstancesOf(StorageAttributes::class, $contents);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function receiving_an_invalid_windows_listing(): void\n    {\n        $response = [\n            '05-23-15  12:09PM    file2.txt',\n        ];\n        mock_function('ftp_rawlist', $response);\n\n        $this->expectException(InvalidListResponseReceived::class);\n\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            iterator_to_array($adapter->listContents('/', false), false);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function getting_an_invalid_listing_response_for_unix_listings(): void\n    {\n        $response = [\n            'total 1',\n            '-rw-r--r--   1 ftp           409 Aug 19 09:01 file1.txt',\n        ];\n        mock_function('ftp_rawlist', $response);\n\n        $this->expectException(InvalidListResponseReceived::class);\n\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            iterator_to_array($adapter->listContents('/', false), false);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_get_the_file_size_of_a_directory(): void\n    {\n        $adapter = $this->adapter();\n\n        $this->runScenario(function () use ($adapter) {\n            $adapter->createDirectory('directory_name', new Config());\n        });\n\n        $this->expectException(UnableToRetrieveMetadata::class);\n\n        $this->runScenario(function () use ($adapter) {\n            $adapter->fileSize('directory_name');\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function formatting_non_manual_recursive_listings(): void\n    {\n        $response = [\n            'drwxr-xr-x   4 ftp      ftp          4096 Nov 24 13:58 .',\n            'drwxr-xr-x  16 ftp      ftp          4096 Sep  2 13:01 ..',\n            'drwxr-xr-x   2 ftp      ftp          4096 Oct 13  2012 cgi-bin',\n            'drwxr-xr-x   2 ftp      ftp          4096 Nov 24 13:59 folder',\n            '-rw-r--r--   1 ftp      ftp           409 Oct 13  2012 index.html',\n            '',\n            'somewhere/cgi-bin:',\n            'drwxr-xr-x   2 ftp      ftp          4096 Oct 13  2012 .',\n            'drwxr-xr-x   4 ftp      ftp          4096 Nov 24 13:58 ..',\n            '',\n            'somewhere/folder:',\n            'drwxr-xr-x   2 ftp      ftp          4096 Nov 24 13:59 .',\n            'drwxr-xr-x   4 ftp      ftp          4096 Nov 24 13:58 ..',\n            '-rw-r--r--   1 ftp      ftp             0 Nov 24 13:59 dummy.txt',\n        ];\n\n        mock_function('ftp_rawlist', $response);\n\n        $options = FtpConnectionOptions::fromArray([\n           'host' => 'localhost',\n           'port' => 2121,\n           'timestampsOnUnixListingsEnabled' => true,\n           'recurseManually' => false,\n           'root' => '/home/foo/upload/',\n           'username' => 'foo',\n           'password' => 'pass',\n       ]);\n\n        $this->runScenario(function () use ($options) {\n            $adapter = new FtpAdapter($options);\n\n            $contents = iterator_to_array($adapter->listContents('somewhere', true), false);\n\n            $this->assertCount(4, $contents);\n            $this->assertContainsOnlyInstancesOf(StorageAttributes::class, $contents);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function filenames_and_dirnames_with_spaces_are_supported(): void\n    {\n        $this->givenWeHaveAnExistingFile('some dirname/file name.txt');\n\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n\n            $this->assertTrue($adapter->fileExists('some dirname/file name.txt'));\n            $contents = iterator_to_array($adapter->listContents('', true));\n            $this->assertCount(2, $contents);\n            $this->assertContainsOnlyInstancesOf(StorageAttributes::class, $contents);\n        });\n    }\n}\n"
  },
  {
    "path": "src/Ftp/FtpConnectionException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse League\\Flysystem\\FilesystemException;\n\ninterface FtpConnectionException extends FilesystemException\n{\n}\n"
  },
  {
    "path": "src/Ftp/FtpConnectionOptions.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse const FTP_BINARY;\n\nclass FtpConnectionOptions\n{\n    public function __construct(\n        private string $host,\n        private string $root,\n        private string $username,\n        private string $password,\n        private int $port = 21,\n        private bool $ssl = false,\n        private int $timeout = 90,\n        private bool $utf8 = false,\n        private bool $passive = true,\n        private int $transferMode = FTP_BINARY,\n        private ?string $systemType = null,\n        private ?bool $ignorePassiveAddress = null,\n        private bool $enableTimestampsOnUnixListings = false,\n        private bool $recurseManually = false,\n        private ?bool $useRawListOptions = null,\n    ) {\n    }\n\n    public function host(): string\n    {\n        return $this->host;\n    }\n\n    public function root(): string\n    {\n        return $this->root;\n    }\n\n    public function username(): string\n    {\n        return $this->username;\n    }\n\n    public function password(): string\n    {\n        return $this->password;\n    }\n\n    public function port(): int\n    {\n        return $this->port;\n    }\n\n    public function ssl(): bool\n    {\n        return $this->ssl;\n    }\n\n    public function timeout(): int\n    {\n        return $this->timeout;\n    }\n\n    public function utf8(): bool\n    {\n        return $this->utf8;\n    }\n\n    public function passive(): bool\n    {\n        return $this->passive;\n    }\n\n    public function transferMode(): int\n    {\n        return $this->transferMode;\n    }\n\n    public function systemType(): ?string\n    {\n        return $this->systemType;\n    }\n\n    public function ignorePassiveAddress(): ?bool\n    {\n        return $this->ignorePassiveAddress;\n    }\n\n    public function timestampsOnUnixListingsEnabled(): bool\n    {\n        return $this->enableTimestampsOnUnixListings;\n    }\n\n    public function recurseManually(): bool\n    {\n        return $this->recurseManually;\n    }\n\n    public function useRawListOptions(): ?bool\n    {\n        return $this->useRawListOptions;\n    }\n\n    public static function fromArray(array $options): FtpConnectionOptions\n    {\n        return new FtpConnectionOptions(\n            $options['host'] ?? 'invalid://host-not-set',\n            $options['root'] ?? '',\n            $options['username'] ?? 'invalid://username-not-set',\n            $options['password'] ?? 'invalid://password-not-set',\n            $options['port'] ?? 21,\n            $options['ssl'] ?? false,\n            $options['timeout'] ?? 90,\n            $options['utf8'] ?? false,\n            $options['passive'] ?? true,\n            $options['transferMode'] ?? FTP_BINARY,\n            $options['systemType'] ?? null,\n            $options['ignorePassiveAddress'] ?? null,\n            $options['timestampsOnUnixListingsEnabled'] ?? false,\n            $options['recurseManually'] ?? true,\n            $options['useRawListOptions'] ?? null,\n        );\n    }\n}\n"
  },
  {
    "path": "src/Ftp/FtpConnectionProvider.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse const FTP_USEPASVADDRESS;\nuse function error_clear_last;\nuse function error_get_last;\n\nclass FtpConnectionProvider implements ConnectionProvider\n{\n    /**\n     * @return resource\n     *\n     * @throws FtpConnectionException\n     */\n    public function createConnection(FtpConnectionOptions $options)\n    {\n        $connection = $this->createConnectionResource(\n            $options->host(),\n            $options->port(),\n            $options->timeout(),\n            $options->ssl()\n        );\n\n        try {\n            $this->authenticate($options, $connection);\n            $this->enableUtf8Mode($options, $connection);\n            $this->ignorePassiveAddress($options, $connection);\n            $this->makeConnectionPassive($options, $connection);\n        } catch (FtpConnectionException $exception) {\n            @ftp_close($connection);\n            throw $exception;\n        }\n\n        return $connection;\n    }\n\n    /**\n     * @return resource\n     */\n    private function createConnectionResource(string $host, int $port, int $timeout, bool $ssl)\n    {\n        error_clear_last();\n        $connection = $ssl ? @ftp_ssl_connect($host, $port, $timeout) : @ftp_connect($host, $port, $timeout);\n\n        if ($connection === false) {\n            throw UnableToConnectToFtpHost::forHost($host, $port, $ssl, error_get_last()['message'] ?? '');\n        }\n\n        return $connection;\n    }\n\n    /**\n     * @param resource $connection\n     */\n    private function authenticate(FtpConnectionOptions $options, $connection): void\n    {\n        if ( ! @ftp_login($connection, $options->username(), $options->password())) {\n            throw new UnableToAuthenticate();\n        }\n    }\n\n    /**\n     * @param resource $connection\n     */\n    private function enableUtf8Mode(FtpConnectionOptions $options, $connection): void\n    {\n        if ( ! $options->utf8()) {\n            return;\n        }\n\n        $response = @ftp_raw($connection, \"OPTS UTF8 ON\");\n\n        if ( ! in_array(substr($response[0], 0, 3), ['200', '202'])) {\n            throw new UnableToEnableUtf8Mode(\n                'Could not set UTF-8 mode for connection: ' . $options->host() . '::' . $options->port()\n            );\n        }\n    }\n\n    /**\n     * @param resource $connection\n     */\n    private function ignorePassiveAddress(FtpConnectionOptions $options, $connection): void\n    {\n        $ignorePassiveAddress = $options->ignorePassiveAddress();\n\n        if ( ! is_bool($ignorePassiveAddress) || ! defined('FTP_USEPASVADDRESS')) {\n            return;\n        }\n\n        if ( ! @ftp_set_option($connection, FTP_USEPASVADDRESS, ! $ignorePassiveAddress)) {\n            throw UnableToSetFtpOption::whileSettingOption('FTP_USEPASVADDRESS');\n        }\n    }\n\n    /**\n     * @param resource $connection\n     */\n    private function makeConnectionPassive(FtpConnectionOptions $options, $connection): void\n    {\n        if ( ! @ftp_pasv($connection, $options->passive())) {\n            throw new UnableToMakeConnectionPassive(\n                'Could not set passive mode for connection: ' . $options->host() . '::' . $options->port()\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ftp/FtpConnectionProviderTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse League\\Flysystem\\AdapterTestUtilities\\RetryOnTestException;\nuse PHPUnit\\Framework\\TestCase;\n\nuse function ftp_close;\n\n/**\n * @group ftp\n */\nclass FtpConnectionProviderTest extends TestCase\n{\n    use RetryOnTestException;\n\n    /**\n     * @var FtpConnectionProvider\n     */\n    private $connectionProvider;\n\n    protected function setUp(): void\n    {\n        $this->retryOnException(UnableToConnectToFtpHost::class);\n    }\n\n    /**\n     * @before\n     */\n    public function setupConnectionProvider(): void\n    {\n        $this->connectionProvider = new FtpConnectionProvider();\n    }\n\n    /**\n     * @after\n     */\n    public function resetFunctionMocks(): void\n    {\n        reset_function_mocks();\n    }\n\n    /**\n     * @test\n     */\n    public function connecting_successfully(): void\n    {\n        $options = FtpConnectionOptions::fromArray([\n            'host' => 'localhost',\n            'port' => 2121,\n            'utf8' => true,\n            'passive' => true,\n            'ignorePassiveAddress' => true,\n            'root' => '/home/foo/upload',\n            'username' => 'foo',\n            'password' => 'pass',\n        ]);\n\n        $this->runScenario(function () use ($options) {\n            $connection = $this->connectionProvider->createConnection($options);\n            $this->assertTrue(ftp_close($connection));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_enable_uft8_mode(): void\n    {\n        $options = FtpConnectionOptions::fromArray([\n            'host' => 'localhost',\n            'port' => 2121,\n            'utf8' => true,\n            'root' => '/home/foo/upload',\n            'username' => 'foo',\n            'password' => 'pass',\n       ]);\n\n        mock_function('ftp_raw', ['Error']);\n\n        $this->expectException(UnableToEnableUtf8Mode::class);\n\n        $this->runScenario(function () use ($options) {\n            $this->connectionProvider->createConnection($options);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function uft8_mode_already_active_by_server(): void\n    {\n        $options = FtpConnectionOptions::fromArray([\n            'host' => 'localhost',\n            'port' => 2121,\n            'utf8' => true,\n            'root' => '/home/foo/upload',\n            'username' => 'foo',\n            'password' => 'pass',\n       ]);\n\n        mock_function('ftp_raw', ['202 UTF8 mode is always enabled. No need to send this command.']);\n        $this->expectNotToPerformAssertions();\n\n        $this->runScenario(function () use ($options) {\n            $this->connectionProvider->createConnection($options);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_ignore_the_passive_address(): void\n    {\n        $options = FtpConnectionOptions::fromArray([\n            'host' => 'localhost',\n            'port' => 2121,\n            'ignorePassiveAddress' => true,\n            'root' => '/home/foo/upload',\n            'username' => 'foo',\n            'password' => 'pass',\n       ]);\n\n        mock_function('ftp_set_option', false);\n\n        $this->expectException(UnableToSetFtpOption::class);\n\n        $this->runScenario(function () use ($options) {\n            $this->connectionProvider->createConnection($options);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_make_the_connection_passive(): void\n    {\n        $options = FtpConnectionOptions::fromArray([\n            'host' => 'localhost',\n            'port' => 2121,\n            'utf8' => true,\n            'root' => '/home/foo/upload',\n            'username' => 'foo',\n            'password' => 'pass',\n       ]);\n\n        mock_function('ftp_pasv', false);\n\n        $this->expectException(UnableToMakeConnectionPassive::class);\n\n        $this->runScenario(function () use ($options) {\n            $this->connectionProvider->createConnection($options);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_connect(): void\n    {\n        $this->dontRetryOnException();\n\n        $options = FtpConnectionOptions::fromArray([\n           'host' => 'localhost',\n           'port' => 313131,\n           'root' => '/home/foo/upload',\n           'username' => 'foo',\n           'password' => 'pass',\n        ]);\n\n        $this->expectException(UnableToConnectToFtpHost::class);\n\n        $this->connectionProvider->createConnection($options);\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_connect_over_ssl(): void\n    {\n        $this->dontRetryOnException();\n\n        $options = FtpConnectionOptions::fromArray([\n           'host' => 'localhost',\n           'ssl' => true,\n           'port' => 313131,\n           'root' => '/home/foo/upload',\n           'username' => 'foo',\n           'password' => 'pass',\n        ]);\n\n        $this->expectException(UnableToConnectToFtpHost::class);\n\n        $this->connectionProvider->createConnection($options);\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_authenticate(): void\n    {\n        $options = FtpConnectionOptions::fromArray([\n           'host' => 'localhost',\n           'port' => 2121,\n           'root' => '/home/foo/upload',\n           'username' => 'foo',\n           'password' => 'lolnope',\n       ]);\n\n        $this->expectException(UnableToAuthenticate::class);\n        $this->retryOnException(UnableToConnectToFtpHost::class);\n        $this->runScenario(function () use ($options) {\n            $this->connectionProvider->createConnection($options);\n        });\n    }\n}\n"
  },
  {
    "path": "src/Ftp/FtpdAdapterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse League\\Flysystem\\FilesystemAdapter;\n\n/**\n * @group ftpd\n */\nclass FtpdAdapterTest extends FtpAdapterTestCase\n{\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        $options = FtpConnectionOptions::fromArray([\n           'host' => 'localhost',\n           'port' => 2122,\n           'timestampsOnUnixListingsEnabled' => true,\n           'root' => '/',\n           'username' => 'foo',\n           'password' => 'pass',\n       ]);\n\n        static::$connectivityChecker = new ConnectivityCheckerThatCanFail(new NoopCommandConnectivityChecker());\n\n        return new FtpAdapter($options, null, static::$connectivityChecker);\n    }\n}\n"
  },
  {
    "path": "src/Ftp/InvalidListResponseReceived.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse League\\Flysystem\\FilesystemException;\nuse RuntimeException;\n\nclass InvalidListResponseReceived extends RuntimeException implements FilesystemException\n{\n}\n"
  },
  {
    "path": "src/Ftp/LICENSE",
    "content": "Copyright (c) 2013-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/Ftp/NoopCommandConnectivityChecker.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse TypeError;\nuse ValueError;\n\nclass NoopCommandConnectivityChecker implements ConnectivityChecker\n{\n    public function isConnected($connection): bool\n    {\n        // @codeCoverageIgnoreStart\n        try {\n            $response = @ftp_raw($connection, 'NOOP');\n        } catch (TypeError | ValueError $typeError) {\n            return false;\n        }\n        // @codeCoverageIgnoreEnd\n\n        $responseCode = $response ? (int) preg_replace('/\\D/', '', implode('', $response)) : false;\n\n        return $responseCode === 200;\n    }\n}\n"
  },
  {
    "path": "src/Ftp/NoopCommandConnectivityCheckerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse League\\Flysystem\\AdapterTestUtilities\\RetryOnTestException;\nuse PHPUnit\\Framework\\TestCase;\n\n/**\n * @group ftp\n */\nclass NoopCommandConnectivityCheckerTest extends TestCase\n{\n    use RetryOnTestException;\n\n    protected function setUp(): void\n    {\n        $this->retryOnException(UnableToConnectToFtpHost::class);\n    }\n\n    /**\n     * @test\n     */\n    public function detecting_a_good_connection(): void\n    {\n        $options = FtpConnectionOptions::fromArray([\n           'host' => 'localhost',\n           'port' => 2121,\n           'root' => '/home/foo/upload',\n           'username' => 'foo',\n           'password' => 'pass',\n       ]);\n        $connection = (new FtpConnectionProvider())->createConnection($options);\n        $connected = (new NoopCommandConnectivityChecker())->isConnected($connection);\n\n        $this->assertTrue($connected);\n    }\n\n    /**\n     * @test\n     */\n    public function detecting_a_closed_connection(): void\n    {\n        $options = FtpConnectionOptions::fromArray([\n            'host' => 'localhost',\n            'port' => 2121,\n            'root' => '/home/foo/upload',\n            'username' => 'foo',\n            'password' => 'pass',\n        ]);\n\n        $this->runScenario(function () use ($options) {\n            $connection = (new FtpConnectionProvider())->createConnection($options);\n            ftp_close($connection);\n\n            $connected = (new NoopCommandConnectivityChecker())->isConnected($connection);\n\n            $this->assertFalse($connected);\n        });\n    }\n}\n"
  },
  {
    "path": "src/Ftp/README.md",
    "content": "## Sub-split of Flysystem for FTP.\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n\n```bash\ncomposer require league/flysystem-ftp\n```\n\nView the [documentation](https://flysystem.thephpleague.com/docs/adapter/ftp/).\n"
  },
  {
    "path": "src/Ftp/RawListFtpConnectivityChecker.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse ValueError;\n\nclass RawListFtpConnectivityChecker implements ConnectivityChecker\n{\n    /**\n     * @inheritDoc\n     */\n    public function isConnected($connection): bool\n    {\n        try {\n            return $connection !== false && @ftp_rawlist($connection, './') !== false;\n        } catch (ValueError $errror) {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ftp/RawListFtpConnectivityCheckerTest.php",
    "content": "<?php\n\nnamespace League\\Flysystem\\Ftp;\n\nuse League\\Flysystem\\AdapterTestUtilities\\RetryOnTestException;\nuse PHPUnit\\Framework\\TestCase;\n\n/**\n * @group ftp\n */\nclass RawListFtpConnectivityCheckerTest extends TestCase\n{\n    use RetryOnTestException;\n    /**\n     * @test\n     */\n    public function detecting_if_a_connection_is_connected(): void\n    {\n        $this->retryOnException(UnableToConnectToFtpHost::class);\n        $this->runScenario(function () {\n            $options = FtpConnectionOptions::fromArray([\n               'host' => 'localhost',\n               'port' => 2121,\n               'root' => '/home/foo/upload/',\n               'username' => 'foo',\n               'password' => 'pass',\n           ]);\n\n            $provider = new FtpConnectionProvider();\n            $connection = $provider->createConnection($options);\n            $connectedChecker = new RawListFtpConnectivityChecker();\n            $this->assertTrue($connectedChecker->isConnected($connection));\n            @ftp_close($connection);\n            $this->assertFalse($connectedChecker->isConnected($connection));\n        });\n    }\n}\n"
  },
  {
    "path": "src/Ftp/StubConnectionProvider.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nclass StubConnectionProvider implements ConnectionProvider\n{\n    public mixed $connection;\n\n    public function __construct(private ConnectionProvider $provider)\n    {\n    }\n\n    public function createConnection(FtpConnectionOptions $options)\n    {\n        return $this->connection = $this->provider->createConnection($options);\n    }\n}\n"
  },
  {
    "path": "src/Ftp/UnableToAuthenticate.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse RuntimeException;\n\nfinal class UnableToAuthenticate extends RuntimeException implements FtpConnectionException\n{\n    public function __construct()\n    {\n        parent::__construct(\"Unable to login/authenticate with FTP\");\n    }\n}\n"
  },
  {
    "path": "src/Ftp/UnableToConnectToFtpHost.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse RuntimeException;\n\nfinal class UnableToConnectToFtpHost extends RuntimeException implements FtpConnectionException\n{\n    public static function forHost(string $host, int $port, bool $ssl, string $reason = ''): UnableToConnectToFtpHost\n    {\n        $usingSsl = $ssl ? ', using ssl' : '';\n\n        return new UnableToConnectToFtpHost(\"Unable to connect to host $host at port $port$usingSsl. $reason\");\n    }\n}\n"
  },
  {
    "path": "src/Ftp/UnableToEnableUtf8Mode.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse RuntimeException;\n\nfinal class UnableToEnableUtf8Mode extends RuntimeException implements FtpConnectionException\n{\n}\n"
  },
  {
    "path": "src/Ftp/UnableToMakeConnectionPassive.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse RuntimeException;\n\nclass UnableToMakeConnectionPassive extends RuntimeException implements FtpConnectionException\n{\n}\n"
  },
  {
    "path": "src/Ftp/UnableToResolveConnectionRoot.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse RuntimeException;\nuse Throwable;\n\nfinal class UnableToResolveConnectionRoot extends RuntimeException implements FtpConnectionException\n{\n    private function __construct(string $message, ?Throwable $previous = null)\n    {\n        parent::__construct($message, 0, $previous);\n    }\n\n    public static function itDoesNotExist(string $root, string $reason = ''): UnableToResolveConnectionRoot\n    {\n        return new UnableToResolveConnectionRoot(\n            'Unable to resolve connection root. It does not seem to exist: ' . $root . \"\\nreason: $reason\"\n        );\n    }\n\n    public static function couldNotGetCurrentDirectory(string $message = ''): UnableToResolveConnectionRoot\n    {\n        return new UnableToResolveConnectionRoot(\n            'Unable to resolve connection root. Could not resolve the current directory. ' . $message\n        );\n    }\n}\n"
  },
  {
    "path": "src/Ftp/UnableToSetFtpOption.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Ftp;\n\nuse RuntimeException;\n\nclass UnableToSetFtpOption extends RuntimeException implements FtpConnectionException\n{\n    public static function whileSettingOption(string $option): UnableToSetFtpOption\n    {\n        return new UnableToSetFtpOption(\"Unable to set FTP option $option.\");\n    }\n}\n"
  },
  {
    "path": "src/Ftp/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-ftp\",\n    \"description\": \"FTP filesystem adapter for Flysystem.\",\n    \"keywords\": [\"filesystem\", \"flysystem\", \"ftp\", \"ftpd\", \"files\", \"file\"],\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"autoload\": {\n        \"psr-4\": {\n            \"League\\\\Flysystem\\\\Ftp\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"ext-ftp\": \"*\",\n        \"league/flysystem\": \"^3.0.0\",\n        \"league/mime-type-detection\": \"^1.0.0\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ]\n}\n"
  },
  {
    "path": "src/GoogleCloudStorage/.gitattributes",
    "content": "* text=auto\n\n/.github export-ignore\n/.gitattributes export-ignore\n/.gitignore export-ignore\n/**/*Test.php export-ignore\n/Stub*.php\nREADME.md\n"
  },
  {
    "path": "src/GoogleCloudStorage/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/GoogleCloudStorage/GoogleCloudStorageAdapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\GoogleCloudStorage;\n\nuse DateTimeInterface;\nuse Google\\Cloud\\Core\\Exception\\NotFoundException;\nuse Google\\Cloud\\Storage\\Bucket;\nuse Google\\Cloud\\Storage\\StorageObject;\nuse League\\Flysystem\\ChecksumAlgoIsNotSupported;\nuse League\\Flysystem\\ChecksumProvider;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\StorageAttributes;\nuse League\\Flysystem\\UnableToCheckDirectoryExistence;\nuse League\\Flysystem\\UnableToCheckFileExistence;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToGenerateTemporaryUrl;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToProvideChecksum;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UrlGeneration\\PublicUrlGenerator;\nuse League\\Flysystem\\UrlGeneration\\TemporaryUrlGenerator;\nuse League\\Flysystem\\Visibility;\nuse League\\MimeTypeDetection\\FinfoMimeTypeDetector;\nuse League\\MimeTypeDetection\\MimeTypeDetector;\nuse LogicException;\nuse Throwable;\nuse function array_key_exists;\nuse function base64_decode;\nuse function bin2hex;\nuse function count;\nuse function rtrim;\nuse function sprintf;\nuse function strlen;\n\nclass GoogleCloudStorageAdapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider, TemporaryUrlGenerator\n{\n    private PathPrefixer $prefixer;\n    private VisibilityHandler $visibilityHandler;\n    private MimeTypeDetector $mimeTypeDetector;\n\n    private static array $algoToInfoMap = [\n        'md5' => 'md5Hash',\n        'crc32c' => 'crc32c',\n        'etag' => 'etag',\n    ];\n\n    public function __construct(\n        private Bucket $bucket,\n        string $prefix = '',\n        ?VisibilityHandler $visibilityHandler = null,\n        private string $defaultVisibility = Visibility::PRIVATE,\n        ?MimeTypeDetector $mimeTypeDetector = null,\n        private bool $streamReads = false,\n    ) {\n        $this->prefixer = new PathPrefixer($prefix);\n        $this->visibilityHandler = $visibilityHandler ?? new PortableVisibilityHandler();\n        $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();\n    }\n\n    public function publicUrl(string $path, Config $config): string\n    {\n        $location = $this->prefixer->prefixPath($path);\n\n        return 'https://storage.googleapis.com/' . $this->bucket->name() . '/' . ltrim($location, '/');\n    }\n\n    public function fileExists(string $path): bool\n    {\n        $prefixedPath = $this->prefixer->prefixPath($path);\n\n        try {\n            return $this->bucket->object($prefixedPath)->exists();\n        } catch (Throwable $exception) {\n            throw UnableToCheckFileExistence::forLocation($path, $exception);\n        }\n    }\n\n    public function directoryExists(string $path): bool\n    {\n        $prefixedPath = $this->prefixer->prefixPath($path);\n        $options = [\n            'delimiter' => '/',\n            'includeTrailingDelimiter' => true,\n        ];\n\n        if (strlen($prefixedPath) > 0) {\n            $options = ['prefix' => rtrim($prefixedPath, '/') . '/'];\n        }\n\n        try {\n            $objects = $this->bucket->objects($options);\n        } catch (Throwable $exception) {\n            throw UnableToCheckDirectoryExistence::forLocation($path, $exception);\n        }\n\n        if (count($objects->prefixes()) > 0) {\n            return true;\n        }\n\n        /** @var StorageObject $object */\n        foreach ($objects as $object) {\n            return true;\n        }\n\n        return false;\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        $this->upload($path, $contents, $config);\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        $this->upload($path, $contents, $config);\n    }\n\n    /**\n     * @param resource|string $contents\n     */\n    private function upload(string $path, $contents, Config $config): void\n    {\n        $prefixedPath = $this->prefixer->prefixPath($path);\n        $options = ['name' => $prefixedPath];\n\n        $visibility = $config->get(Config::OPTION_VISIBILITY, $this->defaultVisibility);\n        $predefinedAcl = $this->visibilityHandler->visibilityToPredefinedAcl($visibility);\n\n        if ($predefinedAcl !== PortableVisibilityHandler::NO_PREDEFINED_VISIBILITY) {\n            $options['predefinedAcl'] = $predefinedAcl;\n        }\n\n        $metadata = $config->get('metadata', []);\n        $shouldDetermineMimetype = $contents !== '' && ! array_key_exists('contentType', $metadata);\n\n        if ($shouldDetermineMimetype && $mimeType = $this->mimeTypeDetector->detectMimeType($path, $contents)) {\n            $metadata['contentType'] = $mimeType;\n        }\n\n        $options['metadata'] = $metadata;\n\n        try {\n            $this->bucket->upload($contents, $options);\n        } catch (Throwable $exception) {\n            throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function read(string $path): string\n    {\n        $prefixedPath = $this->prefixer->prefixPath($path);\n\n        try {\n            return $this->bucket->object($prefixedPath)->downloadAsString();\n        } catch (Throwable $exception) {\n            throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function readStream(string $path)\n    {\n        $prefixedPath = $this->prefixer->prefixPath($path);\n        $options = [];\n        if ($this->streamReads) {\n            $options['restOptions']['stream'] = true;\n        }\n\n        try {\n            $stream = $this->bucket->object($prefixedPath)->downloadAsStream($options)->detach();\n        } catch (Throwable $exception) {\n            throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception);\n        }\n\n        // @codeCoverageIgnoreStart\n        if ( ! is_resource($stream)) {\n            throw UnableToReadFile::fromLocation($path, 'Downloaded object does not contain a file resource.');\n        }\n\n        // @codeCoverageIgnoreEnd\n\n        return $stream;\n    }\n\n    public function delete(string $path): void\n    {\n        try {\n            $prefixedPath = $this->prefixer->prefixPath($path);\n            $this->bucket->object($prefixedPath)->delete();\n        } catch (NotFoundException $thisIsOk) {\n            // this is ok\n        } catch (Throwable $exception) {\n            throw UnableToDeleteFile::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        try {\n            /** @var StorageAttributes[] $listing */\n            $listing = $this->listContents($path, true);\n\n            foreach ($listing as $attributes) {\n                $this->delete($attributes->path());\n            }\n\n            if ($path !== '') {\n                $this->delete(rtrim($path, '/') . '/');\n            }\n        } catch (Throwable $exception) {\n            throw UnableToDeleteDirectory::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        $prefixedPath = $this->prefixer->prefixDirectoryPath($path);\n\n        if ($prefixedPath !== '') {\n            $this->bucket->upload('', ['name' => $prefixedPath]);\n        }\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        try {\n            $prefixedPath = $this->prefixer->prefixPath($path);\n            $object = $this->bucket->object($prefixedPath);\n            $this->visibilityHandler->setVisibility($object, $visibility);\n        } catch (Throwable $previous) {\n            throw UnableToSetVisibility::atLocation($path, $previous->getMessage(), $previous);\n        }\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        try {\n            $prefixedPath = $this->prefixer->prefixPath($path);\n            $object = $this->bucket->object($prefixedPath);\n            $visibility = $this->visibilityHandler->determineVisibility($object);\n\n            return new FileAttributes($path, null, $visibility);\n        } catch (Throwable $exception) {\n            throw UnableToRetrieveMetadata::visibility($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        return $this->fileAttributes($path, 'mimeType');\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        return $this->fileAttributes($path, 'lastModified');\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        return $this->fileAttributes($path, 'fileSize');\n    }\n\n    private function fileAttributes(string $path, string $type): FileAttributes\n    {\n        $exception = null;\n        $prefixedPath = $this->prefixer->prefixPath($path);\n\n        try {\n            $object = $this->bucket->object($prefixedPath);\n            $fileAttributes = $this->storageObjectToStorageAttributes($object);\n        } catch (Throwable $exception) {\n            // passthrough\n        }\n\n        if ( ! isset($fileAttributes) || ! $fileAttributes instanceof FileAttributes || $fileAttributes[$type] === null) {\n            throw UnableToRetrieveMetadata::{$type}($path, isset($exception) ? $exception->getMessage() : '', $exception);\n        }\n\n        return $fileAttributes;\n    }\n\n    public function storageObjectToStorageAttributes(StorageObject $object): StorageAttributes\n    {\n        $path = $this->prefixer->stripPrefix($object->name());\n        $info = $object->info();\n        $lastModified = strtotime($info['updated']);\n\n        if (substr($path, -1, 1) === '/') {\n            return new DirectoryAttributes(rtrim($path, '/'), null, $lastModified);\n        }\n\n        $fileSize = intval($info['size']);\n        $mimeType = $info['contentType'] ?? null;\n\n        return new FileAttributes($path, $fileSize, null, $lastModified, $mimeType, $info);\n    }\n\n    public function listContents(string $path, bool $deep): iterable\n    {\n        $prefixedPath = $this->prefixer->prefixPath($path);\n        $prefixes = $options = [];\n\n        if ($prefixedPath !== '') {\n            $options = ['prefix' => sprintf('%s/', rtrim($prefixedPath, '/'))];\n        }\n\n        if ($deep === false) {\n            $options['delimiter'] = '/';\n            $options['includeTrailingDelimiter'] = true;\n        }\n\n        $objects = $this->bucket->objects($options);\n\n        /** @var StorageObject $object */\n        foreach ($objects as $object) {\n            $prefixes[$this->prefixer->stripDirectoryPrefix($object->name())] = true;\n            yield $this->storageObjectToStorageAttributes($object);\n        }\n\n        foreach ($objects->prefixes() as $prefix) {\n            $prefix = $this->prefixer->stripDirectoryPrefix($prefix);\n\n            if (array_key_exists($prefix, $prefixes)) {\n                continue;\n            }\n\n            $prefixes[$prefix] = true;\n            yield new DirectoryAttributes($prefix);\n        }\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        try {\n            $this->copy($source, $destination, $config);\n            $this->delete($source);\n        } catch (Throwable $exception) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        try {\n            $visibility = $config->get(Config::OPTION_VISIBILITY);\n\n            if ($visibility === null && $config->get(Config::OPTION_RETAIN_VISIBILITY, true)) {\n                $visibility = $this->visibility($source)->visibility();\n            }\n\n            $prefixedSource = $this->prefixer->prefixPath($source);\n            $options = ['name' => $this->prefixer->prefixPath($destination)];\n            $predefinedAcl = $this->visibilityHandler->visibilityToPredefinedAcl(\n                $visibility ?: PortableVisibilityHandler::NO_PREDEFINED_VISIBILITY\n            );\n\n            if ($predefinedAcl !== PortableVisibilityHandler::NO_PREDEFINED_VISIBILITY) {\n                $options['predefinedAcl'] = $predefinedAcl;\n            }\n\n            $this->bucket->object($prefixedSource)->copy($this->bucket, $options);\n        } catch (Throwable $previous) {\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $previous);\n        }\n    }\n\n    public function checksum(string $path, Config $config): string\n    {\n        $algo = $config->get('checksum_algo', 'md5');\n        $header = static::$algoToInfoMap[$algo] ?? null;\n\n        if ($header === null) {\n            throw new ChecksumAlgoIsNotSupported();\n        }\n\n        $prefixedPath = $this->prefixer->prefixPath($path);\n\n        try {\n            $checksum = $this->bucket->object($prefixedPath)->info()[$header]\n                ?? throw new LogicException(\"Header not present: $header\");\n        } catch (Throwable $exception) {\n            throw new UnableToProvideChecksum($exception->getMessage(), $path);\n        }\n\n        return bin2hex(base64_decode($checksum));\n    }\n\n    public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string\n    {\n        $location = $this->prefixer->prefixPath($path);\n\n        try {\n            return $this->bucket->object($location)->signedUrl($expiresAt, $config->get('gcp_signing_options', []));\n        } catch (Throwable $exception) {\n            throw UnableToGenerateTemporaryUrl::dueToError($path, $exception);\n        }\n    }\n}\n"
  },
  {
    "path": "src/GoogleCloudStorage/GoogleCloudStorageAdapterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\GoogleCloudStorage;\n\nuse League\\Flysystem\\AdapterTestUtilities\\FilesystemAdapterTestCase;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToWriteFile;\nuse function getenv;\n\n/**\n * @group gcs\n */\nclass GoogleCloudStorageAdapterTest extends FilesystemAdapterTestCase\n{\n    /**\n     * @var string\n     */\n    private static $adapterPrefix = 'ci';\n    private static StubRiggedBucket $bucket;\n    private static PathPrefixer $prefixer;\n\n    public static function setUpBeforeClass(): void\n    {\n        static::$adapterPrefix = 'frank-ci'; // . bin2hex(random_bytes(10));\n        static::$prefixer = new PathPrefixer(static::$adapterPrefix);\n    }\n\n    protected static function bucketName(): string|array|false\n    {\n        return 'flysystem';\n    }\n\n    protected static function visibilityHandler(): VisibilityHandler\n    {\n        return new PortableVisibilityHandler();\n    }\n\n    public function prefixPath(string $path): string\n    {\n        return static::$prefixer->prefixPath($path);\n    }\n\n    public function prefixDirectoryPath(string $path): string\n    {\n        return static::$prefixer->prefixDirectoryPath($path);\n    }\n\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        if ( ! file_exists(__DIR__ . '/../../google-cloud-service-account.json')) {\n            self::markTestSkipped(\"No google service account found in project root.\");\n        }\n\n        $clientOptions = [\n            'projectId' => getenv('GOOGLE_CLOUD_PROJECT'),\n            'keyFilePath' => __DIR__ . '/../../google-cloud-service-account.json',\n        ];\n        $storageClient = new StubStorageClient($clientOptions);\n        /** @var StubRiggedBucket $bucket */\n        $bucket = $storageClient->bucket(self::bucketName());\n        static::$bucket = $bucket;\n\n        return new GoogleCloudStorageAdapter(\n            $bucket,\n            static::$adapterPrefix,\n            visibilityHandler: self::visibilityHandler(),\n        );\n    }\n\n    /**\n     * @test\n     */\n    public function writing_with_specific_metadata(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('some/path.txt', 'contents', new Config(['metadata' => ['contentType' => 'text/plain+special']]));\n        $mimeType = $adapter->mimeType('some/path.txt')->mimeType();\n        $this->assertEquals('text/plain+special', $mimeType);\n    }\n\n    /**\n     * @test\n     */\n    public function guessing_the_mime_type_when_writing(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('some/config.txt', '<?xml version=\"1.0\" encoding=\"UTF-8\"?><test/>', new Config());\n        $mimeType = $adapter->mimeType('some/config.txt')->mimeType();\n        $this->assertEquals('text/xml', $mimeType);\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_visibility_of_non_existing_file(): void\n    {\n        $this->markTestSkipped(\"\n            Not relevant for this adapter since it's a missing ACL,\n            which turns into a 404 which is the expected outcome\n            of a private visibility. ¯\\_(ツ)_/¯\n        \");\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_unknown_mime_type_of_a_file(): void\n    {\n        $this->markTestSkipped(\"This adapter always returns a mime-type.\");\n    }\n\n    /**\n     * @test\n     */\n    public function listing_a_toplevel_directory(): void\n    {\n        $this->clearStorage();\n        parent::listing_a_toplevel_directory();\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_write_a_file(): void\n    {\n        $adapter = $this->adapter();\n        static::$bucket->failForUpload($this->prefixPath('something.txt'));\n\n        $this->expectException(UnableToWriteFile::class);\n\n        $adapter->write('something.txt', 'contents', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_delete_a_file(): void\n    {\n        $adapter = $this->adapter();\n        static::$bucket->failForObject($this->prefixPath('filename.txt'));\n\n        $this->expectException(UnableToDeleteFile::class);\n\n        $adapter->delete('filename.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_delete_a_directory(): void\n    {\n        $adapter = $this->adapter();\n        $this->givenWeHaveAnExistingFile('dir/filename.txt');\n\n        static::$bucket->failForObject($this->prefixPath('dir/filename.txt'));\n\n        $this->expectException(UnableToDeleteDirectory::class);\n\n        $adapter->deleteDirectory('dir');\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_retrieve_visibility(): void\n    {\n        $adapter = $this->adapter();\n        static::$bucket->failForObject($this->prefixPath('filename.txt'));\n\n        $this->expectException(UnableToRetrieveMetadata::class);\n\n        $adapter->visibility('filename.txt');\n    }\n}\n"
  },
  {
    "path": "src/GoogleCloudStorage/GoogleCloudStorageAdapterWithoutAclTest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\GoogleCloudStorage;\n\nclass GoogleCloudStorageAdapterWithoutAclTest extends GoogleCloudStorageAdapterTest\n{\n    protected static function visibilityHandler(): VisibilityHandler\n    {\n        return new UniformBucketLevelAccessVisibility();\n    }\n\n    protected static function bucketName(): string|array|false\n    {\n        return 'no-acl-bucket-for-ci';\n    }\n}\n"
  },
  {
    "path": "src/GoogleCloudStorage/LICENSE",
    "content": "Copyright (c) 2013-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/GoogleCloudStorage/PortableVisibilityHandler.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\GoogleCloudStorage;\n\nuse Google\\Cloud\\Core\\Exception\\NotFoundException;\nuse Google\\Cloud\\Storage\\Acl;\nuse Google\\Cloud\\Storage\\StorageObject;\nuse League\\Flysystem\\Visibility;\n\nclass PortableVisibilityHandler implements VisibilityHandler\n{\n    public const NO_PREDEFINED_VISIBILITY = 'noPredefinedVisibility';\n    public const ACL_PUBLIC_READ = 'publicRead';\n    public const ACL_AUTHENTICATED_READ = 'authenticatedRead';\n    public const ACL_PRIVATE = 'private';\n    public const ACL_PROJECT_PRIVATE = 'projectPrivate';\n\n    public function __construct(\n        private string $entity = 'allUsers',\n        private string $predefinedPublicAcl = self::ACL_PUBLIC_READ,\n        private string $predefinedPrivateAcl = self::ACL_PROJECT_PRIVATE\n    ) {\n    }\n\n    public function setVisibility(StorageObject $object, string $visibility): void\n    {\n        if ($visibility === Visibility::PRIVATE) {\n            $object->acl()->delete($this->entity);\n        } elseif ($visibility === Visibility::PUBLIC) {\n            $object->acl()->update($this->entity, Acl::ROLE_READER);\n        }\n    }\n\n    public function determineVisibility(StorageObject $object): string\n    {\n        try {\n            $acl = $object->acl()->get(['entity' => 'allUsers']);\n        } catch (NotFoundException $exception) {\n            return Visibility::PRIVATE;\n        }\n\n        return $acl['role'] === Acl::ROLE_READER\n            ? Visibility::PUBLIC\n            : Visibility::PRIVATE;\n    }\n\n    public function visibilityToPredefinedAcl(string $visibility): string\n    {\n        switch ($visibility) {\n            case Visibility::PUBLIC:\n                return $this->predefinedPublicAcl;\n            case self::NO_PREDEFINED_VISIBILITY:\n                return self::NO_PREDEFINED_VISIBILITY;\n            default:\n                return $this->predefinedPrivateAcl;\n        }\n    }\n}\n"
  },
  {
    "path": "src/GoogleCloudStorage/README.md",
    "content": "## Sub-split of Flysystem for Google Cloud Storage (GCS).\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n\n```bash\ncomposer require league/flysystem-google-cloud-storage\n```\n\nView the [documentation](https://flysystem.thephpleague.com/docs/adapter/google-cloud-storage/).\n"
  },
  {
    "path": "src/GoogleCloudStorage/StubRiggedBucket.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\GoogleCloudStorage;\n\nuse Google\\Cloud\\Storage\\Bucket;\nuse LogicException;\nuse Throwable;\n\nclass StubRiggedBucket extends Bucket\n{\n    private array $triggers = [];\n\n    public function failForObject(string $name, ?Throwable $throwable = null): void\n    {\n        $this->setupTrigger('object', $name, $throwable);\n    }\n\n    public function failForUpload(string $name, ?Throwable $throwable = null): void\n    {\n        $this->setupTrigger('upload', $name, $throwable);\n    }\n\n    public function object($name, array $options = [])\n    {\n        $this->pushTrigger('object', $name);\n\n        return parent::object($name, $options);\n    }\n\n    public function upload($data, array $options = [])\n    {\n        $this->pushTrigger('upload', $options['name'] ?? 'unknown-object-name');\n\n        return parent::upload($data, $options);\n    }\n\n    private function setupTrigger(string $method, string $name, ?Throwable $throwable): void\n    {\n        $this->triggers[$method][$name] = $throwable ?? new LogicException('unknown error');\n    }\n\n    private function pushTrigger(string $method, string $name): void\n    {\n        $trigger = $this->triggers[$method][$name] ?? null;\n\n        if ($trigger instanceof Throwable) {\n            unset($this->triggers[$method][$name]);\n            throw $trigger;\n        }\n    }\n}\n"
  },
  {
    "path": "src/GoogleCloudStorage/StubStorageClient.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\GoogleCloudStorage;\n\nuse Google\\Cloud\\Storage\\StorageClient;\nuse function in_array;\n\nclass StubStorageClient extends StorageClient\n{\n    private ?StubRiggedBucket $riggedBucket = null;\n\n    public function __construct(array $config = [])\n    {\n        parent::__construct($config);\n    }\n\n    /**\n     * @var string|null\n     */\n    protected $projectId;\n\n    public function bucket($name, $userProject = false, array $options = [])\n    {\n        $knownBuckets = ['flysystem', 'no-acl-bucket-for-ci'];\n        $isKnownBucket = in_array($name, $knownBuckets);\n\n        if ($isKnownBucket && ! $this->riggedBucket) {\n            $this->riggedBucket = new StubRiggedBucket($this->connection, $name, [\n                'requesterProjectId' => $this->projectId,\n            ]);\n        }\n\n        return $isKnownBucket ? $this->riggedBucket : parent::bucket($name, $userProject);\n    }\n}\n"
  },
  {
    "path": "src/GoogleCloudStorage/UniformBucketLevelAccessVisibility.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\GoogleCloudStorage;\n\nuse Google\\Cloud\\Storage\\StorageObject;\n\nclass UniformBucketLevelAccessVisibility implements VisibilityHandler\n{\n    public const NO_PREDEFINED_VISIBILITY = 'noPredefinedVisibility';\n\n    public function setVisibility(StorageObject $object, string $visibility): void\n    {\n        // noop\n    }\n\n    public function determineVisibility(StorageObject $object): string\n    {\n        return self::NO_PREDEFINED_VISIBILITY;\n    }\n\n    public function visibilityToPredefinedAcl(string $visibility): string\n    {\n        return self::NO_PREDEFINED_VISIBILITY;\n    }\n}\n"
  },
  {
    "path": "src/GoogleCloudStorage/VisibilityHandler.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\GoogleCloudStorage;\n\nuse Google\\Cloud\\Storage\\StorageObject;\n\ninterface VisibilityHandler\n{\n    public function setVisibility(StorageObject $object, string $visibility): void;\n    public function determineVisibility(StorageObject $object): string;\n    public function visibilityToPredefinedAcl(string $visibility): string;\n}\n"
  },
  {
    "path": "src/GoogleCloudStorage/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-google-cloud-storage\",\n    \"description\": \"Google Cloud Storage adapter for Flysystem.\",\n    \"keywords\": [\"filesystem\", \"flysystem\", \"gcs\", \"google cloud storage\"],\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"autoload\": {\n        \"psr-4\": {\n            \"League\\\\Flysystem\\\\GoogleCloudStorage\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"google/cloud-storage\": \"^1.23\",\n        \"league/flysystem\": \"^3.10.0\",\n        \"league/mime-type-detection\": \"^1.0.0\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ]\n}\n"
  },
  {
    "path": "src/GridFS/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n**/*Test.php export-ignore\n**/*Stub.php export-ignore\nREADME.md export-ignore\n"
  },
  {
    "path": "src/GridFS/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n\n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository.\n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/GridFS/GridFSAdapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\GridFS;\n\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\MimeTypeDetection\\FinfoMimeTypeDetector;\nuse League\\MimeTypeDetection\\MimeTypeDetector;\nuse MongoDB\\BSON\\ObjectId;\nuse MongoDB\\BSON\\Regex;\nuse MongoDB\\BSON\\UTCDateTime;\nuse MongoDB\\Driver\\Exception\\Exception;\nuse MongoDB\\GridFS\\Bucket;\nuse MongoDB\\GridFS\\Exception\\FileNotFoundException;\n\n/**\n * @phpstan-type GridFile array{_id:ObjectId, length:int, chunkSize:int, uploadDate:UTCDateTime, filename:string, metadata?:array{contentType?:string, flysystem_visibility?:string}}\n */\nclass GridFSAdapter implements FilesystemAdapter\n{\n    private const METADATA_DIRECTORY = 'flysystem_directory';\n    private const METADATA_VISIBILITY = 'flysystem_visibility';\n    private const METADATA_MIMETYPE = 'contentType';\n    private const TYPEMAP_ARRAY = [\n        'typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array'],\n        'codec' => null,\n    ];\n\n    private Bucket $bucket;\n\n    private PathPrefixer $prefixer;\n\n    private MimeTypeDetector $mimeTypeDetector;\n\n    public function __construct(\n        Bucket $bucket,\n        string $prefix = '',\n        ?MimeTypeDetector $mimeTypeDetector = null,\n    ) {\n        $this->bucket = $bucket;\n        $this->prefixer = new PathPrefixer($prefix);\n        $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();\n    }\n\n    public function fileExists(string $path): bool\n    {\n        $file = $this->findFile($path);\n\n        return $file !== null;\n    }\n\n    public function directoryExists(string $path): bool\n    {\n        // A directory exists if at least one file exists with a path starting with the directory name\n        $files = $this->listContents($path, true);\n\n        foreach ($files as $file) {\n            return true;\n        }\n\n        return false;\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        if (str_ends_with($path, '/')) {\n            throw UnableToWriteFile::atLocation($path, 'file path cannot end with a slash');\n        }\n\n        $filename = $this->prefixer->prefixPath($path);\n        $options = [\n            'metadata' => $config->get('metadata', []),\n        ];\n        if ($visibility = $config->get(Config::OPTION_VISIBILITY)) {\n            $options['metadata'][self::METADATA_VISIBILITY] = $visibility;\n        }\n        if (($mimeType = $config->get('mimetype')) || ($mimeType = $this->mimeTypeDetector->detectMimeType($path, $contents))) {\n            $options['metadata'][self::METADATA_MIMETYPE] = $mimeType;\n        }\n\n        try {\n            $stream = $this->bucket->openUploadStream($filename, $options);\n            fwrite($stream, $contents);\n            fclose($stream);\n        } catch (Exception $exception) {\n            throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        if (str_ends_with($path, '/')) {\n            throw UnableToWriteFile::atLocation($path, 'file path cannot end with a slash');\n        }\n\n        $filename = $this->prefixer->prefixPath($path);\n        $options = [];\n        if ($visibility = $config->get(Config::OPTION_VISIBILITY)) {\n            $options['metadata'][self::METADATA_VISIBILITY] = $visibility;\n        }\n        if (($mimetype = $config->get('mimetype')) || ($mimetype = $this->mimeTypeDetector->detectMimeTypeFromPath($path))) {\n            $options['metadata'][self::METADATA_MIMETYPE] = $mimetype;\n        }\n\n        try {\n            $this->bucket->uploadFromStream($filename, $contents, $options);\n        } catch (Exception $exception) {\n            throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function read(string $path): string\n    {\n        $stream = $this->readStream($path);\n        try {\n            return stream_get_contents($stream);\n        } finally {\n            fclose($stream);\n        }\n    }\n\n    public function readStream(string $path)\n    {\n        if (str_ends_with($path, '/')) {\n            throw UnableToReadFile::fromLocation($path, 'file path cannot end with a slash');\n        }\n\n        try {\n            $filename = $this->prefixer->prefixPath($path);\n\n            return $this->bucket->openDownloadStreamByName($filename);\n        } catch (FileNotFoundException $exception) {\n            throw UnableToReadFile::fromLocation($path, 'file does not exist', $exception);\n        } catch (Exception $exception) {\n            throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    /**\n     * Delete all revisions of the file name, starting with the oldest,\n     * no-op if the file does not exist.\n     *\n     * @throws UnableToDeleteFile\n     */\n    public function delete(string $path): void\n    {\n        if (str_ends_with($path, '/')) {\n            throw UnableToDeleteFile::atLocation($path, 'file path cannot end with a slash');\n        }\n\n        $filename = $this->prefixer->prefixPath($path);\n        try {\n            $this->findAndDelete(['filename' => $filename]);\n        } catch (Exception $exception) {\n            throw UnableToDeleteFile::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        $prefixedPath = $this->prefixer->prefixDirectoryPath($path);\n        try {\n            $this->findAndDelete(['filename' => new Regex('^' . preg_quote($prefixedPath))]);\n        } catch (Exception $exception) {\n            throw UnableToDeleteDirectory::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        $dirname = $this->prefixer->prefixDirectoryPath($path);\n\n        $options = [\n            'metadata' => $config->get('metadata', []) + [self::METADATA_DIRECTORY => true],\n        ];\n\n        if ($visibility = $config->get(Config::OPTION_VISIBILITY)) {\n            $options['metadata'][self::METADATA_VISIBILITY] = $visibility;\n        }\n\n        try {\n            $stream = $this->bucket->openUploadStream($dirname, $options);\n            fwrite($stream, '');\n            fclose($stream);\n        } catch (Exception $exception) {\n            throw UnableToCreateDirectory::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        $file = $this->findFile($path);\n\n        if ($file === null) {\n            throw UnableToSetVisibility::atLocation($path, 'file does not exist');\n        }\n\n        try {\n            $this->bucket->getFilesCollection()->updateOne(\n                ['_id' => $file['_id']],\n                ['$set' => ['metadata.' . self::METADATA_VISIBILITY => $visibility]],\n            );\n        } catch (Exception $exception) {\n            throw UnableToSetVisibility::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        $file = $this->findFile($path);\n\n        if ($file === null) {\n            throw UnableToRetrieveMetadata::mimeType($path, 'file does not exist');\n        }\n\n        return $this->mapFileAttributes($file);\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        if (str_ends_with($path, '/')) {\n            throw UnableToRetrieveMetadata::fileSize($path, 'file path cannot end with a slash');\n        }\n\n        $file = $this->findFile($path);\n        if ($file === null) {\n            throw UnableToRetrieveMetadata::fileSize($path, 'file does not exist');\n        }\n\n        return $this->mapFileAttributes($file);\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        if (str_ends_with($path, '/')) {\n            throw UnableToRetrieveMetadata::mimeType($path, 'file path cannot end with a slash');\n        }\n\n        $file = $this->findFile($path);\n        if ($file === null) {\n            throw UnableToRetrieveMetadata::mimeType($path, 'file does not exist');\n        }\n\n        $attributes = $this->mapFileAttributes($file);\n        if ($attributes->mimeType() === null) {\n            throw UnableToRetrieveMetadata::mimeType($path, 'unknown');\n        }\n\n        return $attributes;\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        if (str_ends_with($path, '/')) {\n            throw UnableToRetrieveMetadata::lastModified($path, 'file path cannot end with a slash');\n        }\n\n        $file = $this->findFile($path);\n        if ($file === null) {\n            throw UnableToRetrieveMetadata::lastModified($path, 'file does not exist');\n        }\n\n        return $this->mapFileAttributes($file);\n    }\n\n    public function listContents(string $path, bool $deep): iterable\n    {\n        $path = $this->prefixer->prefixDirectoryPath($path);\n\n        $pathdeep = 0;\n        // Get the last revision of each file, using the index on the files collection\n        $pipeline = [['$sort' => ['filename' => 1, 'uploadDate' => 1]]];\n        if ($path !== '') {\n            $pathdeep = substr_count($path, '/');\n            // Exclude files that do not start with the expected path\n            $pipeline[] = ['$match' => ['filename' => new Regex('^' . preg_quote($path))]];\n        }\n\n        if ($deep === false) {\n            $pipeline[] = ['$addFields' => ['splitpath' => ['$split' => ['$filename', '/']]]];\n            $pipeline[] = ['$group' => [\n                // The same name could be used as a filename and as part of the path of other files\n                '_id' => [\n                    'basename' => ['$arrayElemAt' => ['$splitpath', $pathdeep]],\n                    'isDir' => ['$ne' => [['$size' => '$splitpath'], $pathdeep + 1]],\n                ],\n                // Get the metadata of the last revision of each file\n                'file' => ['$last' => '$$ROOT'],\n                // The \"lastModified\" date is the date of the last uploaded file in the directory\n                'uploadDate' => ['$max' => '$uploadDate'],\n            ]];\n\n            $files = $this->bucket->getFilesCollection()->aggregate($pipeline, self::TYPEMAP_ARRAY);\n\n            foreach ($files as $file) {\n                if ($file['_id']['isDir']) {\n                    yield new DirectoryAttributes(\n                        $this->prefixer->stripDirectoryPrefix($path . $file['_id']['basename']),\n                        null,\n                        $file['uploadDate']->toDateTime()->getTimestamp(),\n                    );\n                } else {\n                    yield $this->mapFileAttributes($file['file']);\n                }\n            }\n        } else {\n            // Get the metadata of the last revision of each file\n            $pipeline[] = ['$group' => [\n                '_id' => '$filename',\n                'file' => ['$first' => '$$ROOT'],\n            ]];\n\n            $files = $this->bucket->getFilesCollection()->aggregate($pipeline, self::TYPEMAP_ARRAY);\n\n            foreach ($files as $file) {\n                $file = $file['file'];\n                if (str_ends_with($file['filename'], '/')) {\n                    // Empty files with a trailing slash are markers for directories, only for Flysystem\n                    yield new DirectoryAttributes(\n                        $this->prefixer->stripDirectoryPrefix($file['filename']),\n                        $file['metadata'][self::METADATA_VISIBILITY] ?? null,\n                        $file['uploadDate']->toDateTime()->getTimestamp(),\n                        $file,\n                    );\n                } else {\n                    yield $this->mapFileAttributes($file);\n                }\n            }\n        }\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        if ($source === $destination) {\n            return;\n        }\n\n        if ($this->fileExists($destination)) {\n            $this->delete($destination);\n        }\n\n        try {\n            $result = $this->bucket->getFilesCollection()->updateMany(\n                ['filename' => $this->prefixer->prefixPath($source)],\n                ['$set' => ['filename' => $this->prefixer->prefixPath($destination)]],\n            );\n\n            if ($result->getModifiedCount() === 0) {\n                throw UnableToMoveFile::because('file does not exist', $source, $destination);\n            }\n        } catch (Exception $exception) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        $file = $this->findFile($source);\n\n        if ($file === null) {\n            throw UnableToCopyFile::fromLocationTo(\n                $source,\n                $destination,\n            );\n        }\n\n        $options = [];\n        if (($visibility = $config->get(Config::OPTION_VISIBILITY)) || $visibility = $file['metadata'][self::METADATA_VISIBILITY] ?? null) {\n            $options['metadata'][self::METADATA_VISIBILITY] = $visibility;\n        }\n        if (($mimetype = $config->get('mimetype')) || $mimetype = $file['metadata'][self::METADATA_MIMETYPE] ?? null) {\n            $options['metadata'][self::METADATA_MIMETYPE] = $mimetype;\n        }\n\n        try {\n            $stream = $this->bucket->openDownloadStream($file['_id']);\n            $this->bucket->uploadFromStream($this->prefixer->prefixPath($destination), $stream, $options);\n        } catch (Exception $exception) {\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    /**\n     * Get the last revision of the file name.\n     *\n     * @return GridFile|null\n     */\n    private function findFile(string $path): ?array\n    {\n        $filename = $this->prefixer->prefixPath($path);\n        $files = $this->bucket->find(\n            ['filename' => $filename],\n            ['sort' => ['uploadDate' => -1], 'limit' => 1] + self::TYPEMAP_ARRAY,\n        );\n\n        return $files->toArray()[0] ?? null;\n    }\n\n    /**\n     * @param GridFile $file\n     */\n    private function mapFileAttributes(array $file): FileAttributes\n    {\n        return new FileAttributes(\n            $this->prefixer->stripPrefix($file['filename']),\n            $file['length'],\n            $file['metadata'][self::METADATA_VISIBILITY] ?? null,\n            $file['uploadDate']->toDateTime()->getTimestamp(),\n            $file['metadata'][self::METADATA_MIMETYPE] ?? null,\n            $file,\n        );\n    }\n\n    /**\n     * @throws Exception\n     */\n    private function findAndDelete(array $filter): void\n    {\n        $files = $this->bucket->find(\n            $filter,\n            ['sort' => ['uploadDate' => 1], 'projection' => ['_id' => 1]] + self::TYPEMAP_ARRAY,\n        );\n\n        foreach ($files as $file) {\n            try {\n                $this->bucket->delete($file['_id']);\n            } catch (FileNotFoundException) {\n                // Ignore error due to race condition\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/GridFS/GridFSAdapterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\GridFS;\n\nuse League\\Flysystem\\AdapterTestUtilities\\FilesystemAdapterTestCase as TestCase;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToWriteFile;\nuse MongoDB\\Client;\nuse MongoDB\\Database;\nuse function getenv;\n\n/**\n * @group gridfs\n *\n * @method GridFSAdapter adapter()\n */\nclass GridFSAdapterTest extends TestCase\n{\n    /**\n     * @var string\n     */\n    private static $adapterPrefix = 'test-prefix';\n\n    public static function tearDownAfterClass(): void\n    {\n        self::getDatabase()->drop();\n\n        parent::tearDownAfterClass();\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_contains_extra_metadata(): void\n    {\n        $adapter = $this->adapter();\n\n        $this->runScenario(function () use ($adapter) {\n            $this->givenWeHaveAnExistingFile('file.txt');\n            $fileAttributes = $adapter->lastModified('file.txt');\n            $extra = $fileAttributes->extraMetadata();\n            $this->assertArrayHasKey('_id', $extra);\n            $this->assertArrayHasKey('filename', $extra);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_last_modified_of_a_directory(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n\n        $adapter = $this->adapter();\n\n        $this->runScenario(function () use ($adapter) {\n            $adapter->createDirectory('path', new Config());\n            $adapter->lastModified('path/');\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_mime_type_of_a_directory(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n\n        $adapter = $this->adapter();\n\n        $this->runScenario(function () use ($adapter) {\n            $adapter->createDirectory('path', new Config());\n            $adapter->mimeType('path/');\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function reading_a_file_with_trailing_slash(): void\n    {\n        $this->expectException(UnableToReadFile::class);\n        $this->adapter()->read('foo/');\n    }\n\n    /**\n     * @test\n     */\n    public function reading_a_file_stream_with_trailing_slash(): void\n    {\n        $this->expectException(UnableToReadFile::class);\n        $this->adapter()->readStream('foo/');\n    }\n\n    /**\n     * @test\n     */\n    public function writing_a_file_with_trailing_slash(): void\n    {\n        $this->expectException(UnableToWriteFile::class);\n        $this->adapter()->write('foo/', 'contents', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function writing_a_file_stream_with_trailing_slash(): void\n    {\n        $this->expectException(UnableToWriteFile::class);\n        $writeStream = stream_with_contents('contents');\n        $this->adapter()->writeStream('foo/', $writeStream, new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function writing_a_file_with_a_invalid_stream(): void\n    {\n        $this->expectException(UnableToWriteFile::class);\n        // @phpstan-ignore argument.type\n        $this->adapter()->writeStream('file.txt', 'foo', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function delete_a_file_with_trailing_slash(): void\n    {\n        $this->expectException(UnableToDeleteFile::class);\n        $this->adapter()->delete('foo/');\n    }\n\n    /**\n     * @test\n     */\n    public function reading_last_revision(): void\n    {\n        $this->runScenario(\n            function () {\n                $this->givenWeHaveAnExistingFile('file.txt', 'version 1');\n                usleep(1000);\n                $this->givenWeHaveAnExistingFile('file.txt', 'version 2');\n\n                $this->assertSame('version 2', $this->adapter()->read('file.txt'));\n            }\n        );\n    }\n\n    /**\n     * @testWith [false]\n     *           [true]\n     *\n     * @test\n     */\n    public function listing_contents_last_revision(bool $deep): void\n    {\n        $this->runScenario(\n            function () use ($deep) {\n                $this->givenWeHaveAnExistingFile('file.txt', 'version 1');\n                usleep(1000);\n                $this->givenWeHaveAnExistingFile('file.txt', 'version 2');\n\n                $files = $this->adapter()->listContents('', $deep);\n                $files = iterator_to_array($files);\n\n                $this->assertCount(1, $files);\n                $file = $files[0];\n                $this->assertInstanceOf(FileAttributes::class, $file);\n                $this->assertSame('file.txt', $file->path());\n            }\n        );\n    }\n\n    /**\n     * @test\n     */\n    public function listing_contents_directory_with_multiple_files(): void\n    {\n        $this->runScenario(\n            function () {\n                $this->givenWeHaveAnExistingFile('some/file-1.txt');\n                $this->givenWeHaveAnExistingFile('some/file-2.txt');\n                $this->givenWeHaveAnExistingFile('some/other/file-1.txt');\n\n                $files = $this->adapter()->listContents('', false);\n                $files = iterator_to_array($files);\n\n                $this->assertCount(1, $files);\n                $file = $files[0];\n                $this->assertInstanceOf(DirectoryAttributes::class, $file);\n                $this->assertSame('some', $file->path());\n            }\n        );\n    }\n\n    /**\n     * @test\n     */\n    public function delete_all_revisions(): void\n    {\n        $this->runScenario(\n            function () {\n                $this->givenWeHaveAnExistingFile('file.txt', 'version 1');\n                usleep(1000);\n                $this->givenWeHaveAnExistingFile('file.txt', 'version 2');\n                usleep(1000);\n                $this->givenWeHaveAnExistingFile('file.txt', 'version 3');\n\n                $this->adapter()->delete('file.txt');\n\n                $this->assertFalse($this->adapter()->fileExists('file.txt'), 'File does not exist');\n            }\n        );\n    }\n\n    /**\n     * @test\n     */\n    public function move_all_revisions(): void\n    {\n        $this->runScenario(\n            function () {\n                $this->givenWeHaveAnExistingFile('file.txt', 'version 1');\n                usleep(1000);\n                $this->givenWeHaveAnExistingFile('file.txt', 'version 2');\n                usleep(1000);\n                $this->givenWeHaveAnExistingFile('file.txt', 'version 3');\n\n                $this->adapter()->move('file.txt', 'destination.txt', new Config());\n\n                $this->assertFalse($this->adapter()->fileExists('file.txt'));\n                $this->assertSame($this->adapter()->read('destination.txt'), 'version 3');\n            }\n        );\n    }\n\n    protected function tearDown(): void\n    {\n        self::getDatabase()->selectGridFSBucket()->drop();\n\n        parent::tearDown();\n    }\n\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        $bucket = self::getDatabase()->selectGridFSBucket();\n        $prefix = getenv('FLYSYSTEM_MONGODB_PREFIX') ?: self::$adapterPrefix;\n\n        return new GridFSAdapter($bucket, $prefix);\n    }\n\n    private static function getDatabase(): Database\n    {\n        $uri = getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1:27017/';\n        $client = new Client($uri);\n\n        return $client->selectDatabase(getenv('MONGODB_DATABASE') ?: 'flysystem_tests');\n    }\n}\n"
  },
  {
    "path": "src/GridFS/LICENSE",
    "content": "Copyright (c) 2024-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/GridFS/README.md",
    "content": "## Sub-split for Flysystem's MongoDB GridFS Adapter\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n\n```bash\ncomposer require league/flysystem-gridfs\n```\n\nView the [documentation](https://flysystem.thephpleague.com/docs/adapter/gridfs/).\n"
  },
  {
    "path": "src/GridFS/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-gridfs\",\n    \"autoload\": {\n        \"psr-4\": {\n            \"League\\\\Flysystem\\\\GridFS\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"ext-mongodb\": \"^1.3|^2\",\n        \"league/flysystem\": \"^3.10.0\",\n        \"mongodb/mongodb\": \"^1.2|^2\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        },\n        {\n            \"name\": \"MongoDB PHP\",\n            \"email\": \"driver-php@mongodb.com\"\n        }\n    ]\n}\n"
  },
  {
    "path": "src/InMemory/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n**/*Test.php export-ignore\nREADME.md export-ignore\n"
  },
  {
    "path": "src/InMemory/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/InMemory/InMemoryFile.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\InMemory;\n\nuse const FILEINFO_MIME_TYPE;\nuse finfo;\n\n/**\n * @internal\n */\nclass InMemoryFile\n{\n    private string $contents = '';\n    private int $lastModified = 0;\n    private ?string $visibility = null;\n\n    public function updateContents(string $contents, ?int $timestamp): void\n    {\n        $this->contents = $contents;\n        $this->lastModified = $timestamp ?? time();\n    }\n\n    public function lastModified(): int\n    {\n        return $this->lastModified;\n    }\n\n    public function withLastModified(int $lastModified): self\n    {\n        $clone = clone $this;\n        $clone->lastModified = $lastModified;\n\n        return $clone;\n    }\n\n    public function read(): string\n    {\n        return $this->contents;\n    }\n\n    /**\n     * @return resource\n     */\n    public function readStream()\n    {\n        /** @var resource $stream */\n        $stream = fopen('php://temp', 'w+b');\n        fwrite($stream, $this->contents);\n        rewind($stream);\n\n        return $stream;\n    }\n\n    public function fileSize(): int\n    {\n        return strlen($this->contents);\n    }\n\n    public function mimeType(): string\n    {\n        return (string) (new finfo(FILEINFO_MIME_TYPE))->buffer($this->contents);\n    }\n\n    public function setVisibility(string $visibility): void\n    {\n        $this->visibility = $visibility;\n    }\n\n    public function visibility(): ?string\n    {\n        return $this->visibility;\n    }\n}\n"
  },
  {
    "path": "src/InMemory/InMemoryFilesystemAdapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\InMemory;\n\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\Visibility;\nuse League\\MimeTypeDetection\\FinfoMimeTypeDetector;\nuse League\\MimeTypeDetection\\MimeTypeDetector;\n\nuse function array_keys;\nuse function rtrim;\n\nclass InMemoryFilesystemAdapter implements FilesystemAdapter\n{\n    public const DUMMY_FILE_FOR_FORCED_LISTING_IN_FLYSYSTEM_TEST = '______DUMMY_FILE_FOR_FORCED_LISTING_IN_FLYSYSTEM_TEST';\n\n    /**\n     * @var InMemoryFile[]\n     */\n    private array $files = [];\n    private MimeTypeDetector $mimeTypeDetector;\n\n    public function __construct(\n        private string $defaultVisibility = Visibility::PUBLIC,\n        ?MimeTypeDetector $mimeTypeDetector = null\n    ) {\n        $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();\n    }\n\n    public function fileExists(string $path): bool\n    {\n        return array_key_exists($this->preparePath($path), $this->files);\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        $path = $this->preparePath($path);\n        $file = $this->files[$path] = $this->files[$path] ?? new InMemoryFile();\n        $file->updateContents($contents, $config->get('timestamp'));\n\n        $visibility = $config->get(Config::OPTION_VISIBILITY, $this->defaultVisibility);\n        $file->setVisibility($visibility);\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        $this->write($path, (string) stream_get_contents($contents), $config);\n    }\n\n    public function read(string $path): string\n    {\n        $path = $this->preparePath($path);\n\n        if (array_key_exists($path, $this->files) === false) {\n            throw UnableToReadFile::fromLocation($path, 'file does not exist');\n        }\n\n        return $this->files[$path]->read();\n    }\n\n    public function readStream(string $path)\n    {\n        $path = $this->preparePath($path);\n\n        if (array_key_exists($path, $this->files) === false) {\n            throw UnableToReadFile::fromLocation($path, 'file does not exist');\n        }\n\n        return $this->files[$path]->readStream();\n    }\n\n    public function delete(string $path): void\n    {\n        unset($this->files[$this->preparePath($path)]);\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        $path = $this->preparePath($path);\n        $path = rtrim($path, '/') . '/';\n\n        foreach (array_keys($this->files) as $filePath) {\n            if (str_starts_with($filePath, $path)) {\n                unset($this->files[$filePath]);\n            }\n        }\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        $filePath = rtrim($path, '/') . '/' . self::DUMMY_FILE_FOR_FORCED_LISTING_IN_FLYSYSTEM_TEST;\n        $this->write($filePath, '', $config);\n    }\n\n    public function directoryExists(string $path): bool\n    {\n        $path = $this->preparePath($path);\n        $path = rtrim($path, '/') . '/';\n\n        foreach (array_keys($this->files) as $filePath) {\n            if (str_starts_with($filePath, $path)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        $path = $this->preparePath($path);\n\n        if (array_key_exists($path, $this->files) === false) {\n            throw UnableToSetVisibility::atLocation($path, 'file does not exist');\n        }\n\n        $this->files[$path]->setVisibility($visibility);\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        $path = $this->preparePath($path);\n\n        if (array_key_exists($path, $this->files) === false) {\n            throw UnableToRetrieveMetadata::visibility($path, 'file does not exist');\n        }\n\n        return new FileAttributes($path, null, $this->files[$path]->visibility());\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        $preparedPath = $this->preparePath($path);\n\n        if (array_key_exists($preparedPath, $this->files) === false) {\n            throw UnableToRetrieveMetadata::mimeType($path, 'file does not exist');\n        }\n\n        $mimeType = $this->mimeTypeDetector->detectMimeType($path, $this->files[$preparedPath]->read());\n\n        if ($mimeType === null) {\n            throw UnableToRetrieveMetadata::mimeType($path);\n        }\n\n        return new FileAttributes($preparedPath, null, null, null, $mimeType);\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        $path = $this->preparePath($path);\n\n        if (array_key_exists($path, $this->files) === false) {\n            throw UnableToRetrieveMetadata::lastModified($path, 'file does not exist');\n        }\n\n        return new FileAttributes($path, null, null, $this->files[$path]->lastModified());\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        $path = $this->preparePath($path);\n\n        if (array_key_exists($path, $this->files) === false) {\n            throw UnableToRetrieveMetadata::fileSize($path, 'file does not exist');\n        }\n\n        return new FileAttributes($path, $this->files[$path]->fileSize());\n    }\n\n    public function listContents(string $path, bool $deep): iterable\n    {\n        $prefix = rtrim($this->preparePath($path), '/') . '/';\n        $prefixLength = strlen($prefix);\n        $listedDirectories = [];\n\n        foreach ($this->files as $filePath => $file) {\n            if (str_starts_with($filePath, $prefix)) {\n                $subPath = substr($filePath, $prefixLength);\n                $dirname = dirname($subPath);\n\n                if ($dirname !== '.') {\n                    $parts = explode('/', $dirname);\n                    $dirPath = '';\n\n                    foreach ($parts as $index => $part) {\n                        if ($deep === false && $index >= 1) {\n                            break;\n                        }\n\n                        $dirPath .= $part . '/';\n\n                        if ( ! in_array($dirPath, $listedDirectories, true)) {\n                            $listedDirectories[] = $dirPath;\n                            yield new DirectoryAttributes(trim($prefix . $dirPath, '/'));\n                        }\n                    }\n                }\n\n                $dummyFilename = self::DUMMY_FILE_FOR_FORCED_LISTING_IN_FLYSYSTEM_TEST;\n                if (str_ends_with($filePath, $dummyFilename)) {\n                    continue;\n                }\n\n                if ($deep === true || ! str_contains($subPath, '/')) {\n                    yield new FileAttributes(ltrim($filePath, '/'), $file->fileSize(), $file->visibility(), $file->lastModified(), $file->mimeType());\n                }\n            }\n        }\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        $sourcePath = $this->preparePath($source);\n        $destinationPath = $this->preparePath($destination);\n\n        if ( ! $this->fileExists($source)) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination);\n        }\n\n        if ($sourcePath !== $destinationPath) {\n            $this->files[$destinationPath] = $this->files[$sourcePath];\n            unset($this->files[$sourcePath]);\n        }\n\n        if ($visibility = $config->get(Config::OPTION_VISIBILITY)) {\n            $this->setVisibility($destination, $visibility);\n        }\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        $source = $this->preparePath($source);\n        $destination = $this->preparePath($destination);\n\n        if ( ! $this->fileExists($source)) {\n            throw UnableToCopyFile::fromLocationTo($source, $destination);\n        }\n\n        $lastModified = $config->get('timestamp', time());\n        $this->files[$destination] = $this->files[$source]->withLastModified($lastModified);\n\n        if ($visibility = $config->get(Config::OPTION_VISIBILITY)) {\n            $this->setVisibility($destination, $visibility);\n        }\n    }\n\n    private function preparePath(string $path): string\n    {\n        return '/' . ltrim($path, '/');\n    }\n\n    public function deleteEverything(): void\n    {\n        $this->files = [];\n    }\n}\n"
  },
  {
    "path": "src/InMemory/InMemoryFilesystemAdapterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\InMemory;\n\nuse League\\Flysystem\\AdapterTestUtilities\\FilesystemAdapterTestCase;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\StorageAttributes;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\Visibility;\nuse League\\MimeTypeDetection\\EmptyExtensionToMimeTypeMap;\nuse League\\MimeTypeDetection\\ExtensionMimeTypeDetector;\n\n/**\n * @group in-memory\n */\nclass InMemoryFilesystemAdapterTest extends FilesystemAdapterTestCase\n{\n    const PATH = 'path.txt';\n\n    /**\n     * @before\n     */\n    public function resetFunctionMocks(): void\n    {\n        reset_function_mocks();\n        /** @var InMemoryFilesystemAdapter $filesystemAdapter */\n        $filesystemAdapter = $this->adapter();\n        $filesystemAdapter->deleteEverything();\n    }\n\n    /**\n     * @test\n     */\n    public function getting_mimetype_on_a_non_existing_file(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n        $this->adapter()->mimeType('path.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function getting_last_modified_on_a_non_existing_file(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n        $this->adapter()->lastModified('path.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function getting_file_size_on_a_non_existing_file(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n        $this->adapter()->fileSize('path.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_file(): void\n    {\n        $this->adapter()->write('path.txt', 'contents', new Config());\n        $this->assertTrue($this->adapter()->fileExists('path.txt'));\n        $this->adapter()->delete('path.txt');\n        $this->assertFalse($this->adapter()->fileExists('path.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_directory(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('a/path.txt', 'contents', new Config());\n        $adapter->write('a/b/path.txt', 'contents', new Config());\n        $adapter->write('a/b/c/path.txt', 'contents', new Config());\n        $this->assertTrue($adapter->fileExists('a/b/path.txt'));\n        $this->assertTrue($adapter->fileExists('a/b/c/path.txt'));\n        $adapter->deleteDirectory('a/b');\n        $this->assertTrue($adapter->fileExists('a/path.txt'));\n        $this->assertFalse($adapter->fileExists('a/b/path.txt'));\n        $this->assertFalse($adapter->fileExists('a/b/c/path.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function creating_a_directory_does_nothing(): void\n    {\n        $this->adapter()->createDirectory('something', new Config());\n        $this->assertTrue(true);\n    }\n\n    /**\n     * @test\n     */\n    public function writing_with_a_stream_and_reading_a_file(): void\n    {\n        $handle = stream_with_contents('contents');\n        $this->adapter()->writeStream(self::PATH, $handle, new Config());\n        $contents = $this->adapter()->read(self::PATH);\n        $this->assertEquals('contents', $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function reading_a_stream(): void\n    {\n        $this->adapter()->write(self::PATH, 'contents', new Config());\n        $contents = $this->adapter()->readStream(self::PATH);\n        $this->assertEquals('contents', stream_get_contents($contents));\n        fclose($contents);\n    }\n\n    /**\n     * @test\n     */\n    public function reading_a_non_existing_file(): void\n    {\n        $this->expectException(UnableToReadFile::class);\n        $this->adapter()->read('path.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function stream_reading_a_non_existing_file(): void\n    {\n        $this->expectException(UnableToReadFile::class);\n        $this->adapter()->readStream('path.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function listing_all_files(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('path.txt', 'contents', new Config());\n        $adapter->write('a/path.txt', 'contents', new Config());\n        $adapter->write('a/b/path.txt', 'contents', new Config());\n        /** @var StorageAttributes[] $listing */\n        $listing = iterator_to_array($adapter->listContents('/', true));\n        $this->assertCount(5, $listing);\n\n        $expected = [\n            'path.txt' => StorageAttributes::TYPE_FILE,\n            'a/path.txt' => StorageAttributes::TYPE_FILE,\n            'a/b/path.txt' => StorageAttributes::TYPE_FILE,\n            'a' => StorageAttributes::TYPE_DIRECTORY,\n            'a/b' => StorageAttributes::TYPE_DIRECTORY,\n        ];\n\n        foreach ($listing as $item) {\n            $this->assertArrayHasKey($item->path(), $expected);\n            $this->assertEquals($item->type(), $expected[$item->path()]);\n        }\n    }\n\n    /**\n     * @test\n     */\n    public function listing_non_recursive(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('path.txt', 'contents', new Config());\n        $adapter->write('a/path.txt', 'contents', new Config());\n        $adapter->write('a/b/path.txt', 'contents', new Config());\n        $listing = iterator_to_array($adapter->listContents('/', false));\n        $this->assertCount(2, $listing);\n    }\n\n    /**\n     * @test\n     */\n    public function moving_a_file_successfully(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('path.txt', 'contents', new Config());\n        $adapter->move('path.txt', 'new-path.txt', new Config());\n        $this->assertFalse($adapter->fileExists('path.txt'));\n        $this->assertTrue($adapter->fileExists('new-path.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function trying_to_move_a_non_existing_file(): void\n    {\n        $this->expectException(UnableToMoveFile::class);\n        $this->adapter()->move('path.txt', 'new-path.txt', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file_successfully(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('path.txt', 'contents', new Config());\n        $adapter->copy('path.txt', 'new-path.txt', new Config());\n        $this->assertTrue($adapter->fileExists('path.txt'));\n        $this->assertTrue($adapter->fileExists('new-path.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function trying_to_copy_a_non_existing_file(): void\n    {\n        $this->expectException(UnableToCopyFile::class);\n        $this->adapter()->copy('path.txt', 'new-path.txt', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function not_listing_directory_placeholders(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->createDirectory('directory', new Config());\n\n        $contents = iterator_to_array($adapter->listContents('', true));\n        $this->assertCount(1, $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function checking_for_metadata(): void\n    {\n        mock_function('time', 1234);\n        $adapter = $this->adapter();\n        $adapter->write(\n            self::PATH,\n            (string) file_get_contents(__DIR__ . '/../AdapterTestUtilities/test_files/flysystem.svg'),\n            new Config()\n        );\n\n        $this->assertTrue($adapter->fileExists(self::PATH));\n        $this->assertEquals(754, $adapter->fileSize(self::PATH)->fileSize());\n        $this->assertEquals(1234, $adapter->lastModified(self::PATH)->lastModified());\n        $this->assertStringStartsWith('image/svg+xml', $adapter->mimeType(self::PATH)->mimeType());\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_unknown_mime_type_of_a_file(): void\n    {\n        $this->useAdapter(new InMemoryFilesystemAdapter(Visibility::PUBLIC, new ExtensionMimeTypeDetector(new EmptyExtensionToMimeTypeMap())));\n        parent::fetching_unknown_mime_type_of_a_file();\n    }\n\n    /**\n     * @test\n     */\n    public function using_custom_timestamp(): void\n    {\n        $adapter = $this->adapter();\n\n        $now = 100;\n        $adapter->write('file.txt', 'contents', new Config(['timestamp' => $now]));\n        $this->assertEquals($now, $adapter->lastModified('file.txt')->lastModified());\n\n        $earlier = 50;\n        $adapter->copy('file.txt', 'new_file.txt', new Config(['timestamp' => $earlier]));\n        $this->assertEquals($earlier, $adapter->lastModified('new_file.txt')->lastModified());\n    }\n\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        return new InMemoryFilesystemAdapter();\n    }\n}\n"
  },
  {
    "path": "src/InMemory/LICENSE",
    "content": "Copyright (c) 2013-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/InMemory/README.md",
    "content": "## Sub-split of Flysystem for in-memory file storage.\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n\n```bash\ncomposer require league/flysystem-memory\n```\n\nView the [documentation](https://flysystem.thephpleague.com/docs/adapter/in-memory/).\n"
  },
  {
    "path": "src/InMemory/StaticInMemoryAdapterRegistry.php",
    "content": "<?php\n\nnamespace League\\Flysystem\\InMemory;\n\nclass StaticInMemoryAdapterRegistry\n{\n    /** @var array<string, InMemoryFilesystemAdapter> */\n    private static array $filesystems = [];\n\n    public static function get(string $name = 'default'): InMemoryFilesystemAdapter\n    {\n        return static::$filesystems[$name] ??= new InMemoryFilesystemAdapter();\n    }\n\n    public static function deleteAllFilesystems(): void\n    {\n        self::$filesystems = [];\n    }\n}\n"
  },
  {
    "path": "src/InMemory/StaticInMemoryAdapterRegistryTest.php",
    "content": "<?php\n\nnamespace League\\Flysystem\\InMemory;\n\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\FilesystemAdapter;\n\nclass StaticInMemoryAdapterRegistryTest extends InMemoryFilesystemAdapterTest\n{\n    /**\n     * @test\n     */\n    public function using_different_name_to_segment_adapters(): void\n    {\n        $first = StaticInMemoryAdapterRegistry::get();\n        $second = StaticInMemoryAdapterRegistry::get('second');\n\n        $first->write('foo.txt', 'foo', new Config());\n        $second->write('bar.txt', 'bar', new Config());\n\n        $this->assertTrue($first->fileExists('foo.txt'));\n        $this->assertFalse($first->fileExists('bar.txt'));\n        $this->assertTrue($second->fileExists('bar.txt'));\n        $this->assertFalse($second->fileExists('foo.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function files_persist_between_instances(): void\n    {\n        $first = StaticInMemoryAdapterRegistry::get();\n        $second = StaticInMemoryAdapterRegistry::get('second');\n\n        $first->write('foo.txt', 'foo', new Config());\n        $second->write('bar.txt', 'bar', new Config());\n\n        $this->assertTrue($first->fileExists('foo.txt'));\n        $this->assertTrue($second->fileExists('bar.txt'));\n\n        $first = StaticInMemoryAdapterRegistry::get();\n        $second = StaticInMemoryAdapterRegistry::get('second');\n\n        $this->assertTrue($first->fileExists('foo.txt'));\n        $this->assertTrue($second->fileExists('bar.txt'));\n    }\n\n    protected function tearDown(): void\n    {\n        StaticInMemoryAdapterRegistry::deleteAllFilesystems();\n    }\n\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        return StaticInMemoryAdapterRegistry::get();\n    }\n}\n"
  },
  {
    "path": "src/InMemory/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-memory\",\n    \"description\": \"In-memory filesystem adapter for Flysystem.\",\n    \"keywords\": [\"flysystem\", \"filesystem\", \"memory\", \"file\", \"files\"],\n    \"type\": \"library\",\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"autoload\": {\n        \"psr-4\": {\n            \"League\\\\Flysystem\\\\InMemory\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"ext-fileinfo\": \"*\",\n        \"league/flysystem\": \"^3.0.0\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ]\n}\n"
  },
  {
    "path": "src/InvalidStreamProvided.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse InvalidArgumentException as BaseInvalidArgumentException;\n\nclass InvalidStreamProvided extends BaseInvalidArgumentException implements FilesystemException\n{\n}\n"
  },
  {
    "path": "src/InvalidVisibilityProvided.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse InvalidArgumentException;\n\nuse function var_export;\n\nclass InvalidVisibilityProvided extends InvalidArgumentException implements FilesystemException\n{\n    public static function withVisibility(string $visibility, string $expectedMessage): InvalidVisibilityProvided\n    {\n        $provided = var_export($visibility, true);\n        $message = \"Invalid visibility provided. Expected {$expectedMessage}, received {$provided}\";\n\n        throw new InvalidVisibilityProvided($message);\n    }\n}\n"
  },
  {
    "path": "src/Local/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n**/*Test.php export-ignore\nREADME.md export-ignore\n"
  },
  {
    "path": "src/Local/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/Local/FallbackMimeTypeDetector.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Local;\n\nuse League\\MimeTypeDetection\\MimeTypeDetector;\nuse function in_array;\n\nclass FallbackMimeTypeDetector implements MimeTypeDetector\n{\n    private const INCONCLUSIVE_MIME_TYPES = [\n        'application/x-empty',\n        'text/plain',\n        'text/x-asm',\n        'application/octet-stream',\n        'inode/x-empty',\n    ];\n\n    public function __construct(\n        private MimeTypeDetector $detector,\n        private array $inconclusiveMimetypes = self::INCONCLUSIVE_MIME_TYPES,\n        private bool $useInconclusiveMimeTypeFallback = false,\n    ) {\n    }\n\n    public function detectMimeType(string $path, $contents): ?string\n    {\n        return $this->detector->detectMimeType($path, $contents);\n    }\n\n    public function detectMimeTypeFromBuffer(string $contents): ?string\n    {\n        return $this->detector->detectMimeTypeFromBuffer($contents);\n    }\n\n    public function detectMimeTypeFromPath(string $path): ?string\n    {\n        return $this->detector->detectMimeTypeFromPath($path);\n    }\n\n    public function detectMimeTypeFromFile(string $path): ?string\n    {\n        $mimeType = $this->detector->detectMimeTypeFromFile($path);\n\n        if ($mimeType !== null && ! in_array($mimeType, $this->inconclusiveMimetypes)) {\n            return $mimeType;\n        }\n\n        return $this->detector->detectMimeTypeFromPath($path) ?? ($this->useInconclusiveMimeTypeFallback ? $mimeType : null);\n    }\n}\n"
  },
  {
    "path": "src/Local/LICENSE",
    "content": "Copyright (c) 2013-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/Local/LocalFilesystemAdapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Local;\n\nuse const DIRECTORY_SEPARATOR;\nuse const LOCK_EX;\nuse DirectoryIterator;\nuse FilesystemIterator;\nuse Generator;\nuse League\\Flysystem\\ChecksumProvider;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\SymbolicLinkEncountered;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToProvideChecksum;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UnixVisibility\\PortableVisibilityConverter;\nuse League\\Flysystem\\UnixVisibility\\VisibilityConverter;\nuse League\\MimeTypeDetection\\FinfoMimeTypeDetector;\nuse League\\MimeTypeDetection\\MimeTypeDetector;\nuse RecursiveDirectoryIterator;\nuse RecursiveIteratorIterator;\nuse SplFileInfo;\nuse Throwable;\nuse function chmod;\nuse function clearstatcache;\nuse function dirname;\nuse function error_clear_last;\nuse function error_get_last;\nuse function file_exists;\nuse function file_put_contents;\nuse function hash_file;\nuse function is_dir;\nuse function is_file;\nuse function mkdir;\nuse function rename;\n\nclass LocalFilesystemAdapter implements FilesystemAdapter, ChecksumProvider\n{\n    /**\n     * @var int\n     */\n    public const SKIP_LINKS = 0001;\n\n    /**\n     * @var int\n     */\n    public const DISALLOW_LINKS = 0002;\n\n    private PathPrefixer $prefixer;\n    private VisibilityConverter $visibility;\n    private MimeTypeDetector $mimeTypeDetector;\n    private string $rootLocation;\n\n    /**\n     * @var bool\n     */\n    private $rootLocationIsSetup = false;\n\n    public function __construct(\n        string $location,\n        ?VisibilityConverter $visibility = null,\n        private int $writeFlags = LOCK_EX,\n        private int $linkHandling = self::DISALLOW_LINKS,\n        ?MimeTypeDetector $mimeTypeDetector = null,\n        bool $lazyRootCreation = false,\n        bool $useInconclusiveMimeTypeFallback = false,\n    ) {\n        $this->prefixer = new PathPrefixer($location, DIRECTORY_SEPARATOR);\n        $visibility ??= new PortableVisibilityConverter();\n        $this->visibility = $visibility;\n        $this->rootLocation = $location;\n        $this->mimeTypeDetector = $mimeTypeDetector ?? new FallbackMimeTypeDetector(\n            detector: new FinfoMimeTypeDetector(),\n            useInconclusiveMimeTypeFallback: $useInconclusiveMimeTypeFallback,\n        );\n\n        if ( ! $lazyRootCreation) {\n            $this->ensureRootDirectoryExists();\n        }\n    }\n\n    private function ensureRootDirectoryExists(): void\n    {\n        if ($this->rootLocationIsSetup) {\n            return;\n        }\n\n        $this->ensureDirectoryExists($this->rootLocation, $this->visibility->defaultForDirectories());\n        $this->rootLocationIsSetup = true;\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        $this->writeToFile($path, $contents, $config);\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        $this->writeToFile($path, $contents, $config);\n    }\n\n    /**\n     * @param resource|string $contents\n     */\n    private function writeToFile(string $path, $contents, Config $config): void\n    {\n        $prefixedLocation = $this->prefixer->prefixPath($path);\n        $this->ensureRootDirectoryExists();\n        $this->ensureDirectoryExists(\n            dirname($prefixedLocation),\n            $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY))\n        );\n        error_clear_last();\n\n        if (@file_put_contents($prefixedLocation, $contents, $this->writeFlags) === false) {\n            throw UnableToWriteFile::atLocation($path, error_get_last()['message'] ?? '');\n        }\n\n        if ($visibility = $config->get(Config::OPTION_VISIBILITY)) {\n            $this->setVisibility($path, (string) $visibility);\n        }\n    }\n\n    public function delete(string $path): void\n    {\n        $location = $this->prefixer->prefixPath($path);\n\n        if ( ! file_exists($location)) {\n            return;\n        }\n\n        error_clear_last();\n\n        if ( ! @unlink($location)) {\n            throw UnableToDeleteFile::atLocation($location, error_get_last()['message'] ?? '');\n        }\n    }\n\n    public function deleteDirectory(string $prefix): void\n    {\n        $location = $this->prefixer->prefixPath($prefix);\n\n        if ( ! is_dir($location)) {\n            return;\n        }\n\n        $contents = $this->listDirectoryRecursively($location, RecursiveIteratorIterator::CHILD_FIRST);\n\n        /** @var SplFileInfo $file */\n        foreach ($contents as $file) {\n            if ( ! $this->deleteFileInfoObject($file)) {\n                throw UnableToDeleteDirectory::atLocation($prefix, \"Unable to delete file at \" . $file->getPathname());\n            }\n        }\n\n        unset($contents);\n\n        if ( ! @rmdir($location)) {\n            throw UnableToDeleteDirectory::atLocation($prefix, error_get_last()['message'] ?? '');\n        }\n    }\n\n    private function listDirectoryRecursively(\n        string $path,\n        int $mode = RecursiveIteratorIterator::SELF_FIRST\n    ): Generator {\n        if ( ! is_dir($path)) {\n            return;\n        }\n\n        yield from new RecursiveIteratorIterator(\n            new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),\n            $mode\n        );\n    }\n\n    protected function deleteFileInfoObject(SplFileInfo $file): bool\n    {\n        switch ($file->getType()) {\n            case 'dir':\n                return @rmdir((string) $file->getRealPath());\n            case 'link':\n                return @unlink((string) $file->getPathname());\n            default:\n                return @unlink((string) $file->getRealPath());\n        }\n    }\n\n    public function listContents(string $path, bool $deep): iterable\n    {\n        $location = $this->prefixer->prefixPath($path);\n\n        if ( ! is_dir($location)) {\n            return;\n        }\n\n        /** @var SplFileInfo[] $iterator */\n        $iterator = $deep ? $this->listDirectoryRecursively($location) : $this->listDirectory($location);\n\n        foreach ($iterator as $fileInfo) {\n            $pathName = $fileInfo->getPathname();\n\n            try {\n                if ($fileInfo->isLink()) {\n                    if ($this->linkHandling & self::SKIP_LINKS) {\n                        continue;\n                    }\n                    throw SymbolicLinkEncountered::atLocation($pathName);\n                }\n\n                $path = $this->prefixer->stripPrefix($pathName);\n                $lastModified = $fileInfo->getMTime();\n                $isDirectory = $fileInfo->isDir();\n                $permissions = octdec(substr(sprintf('%o', $fileInfo->getPerms()), -4));\n                $visibility = $isDirectory ? $this->visibility->inverseForDirectory($permissions) : $this->visibility->inverseForFile($permissions);\n\n                yield $isDirectory ? new DirectoryAttributes(str_replace('\\\\', '/', $path), $visibility, $lastModified) : new FileAttributes(\n                    str_replace('\\\\', '/', $path),\n                    $fileInfo->getSize(),\n                    $visibility,\n                    $lastModified\n                );\n            } catch (Throwable $exception) {\n                if (file_exists($pathName)) {\n                    throw $exception;\n                }\n            }\n        }\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        $sourcePath = $this->prefixer->prefixPath($source);\n        $destinationPath = $this->prefixer->prefixPath($destination);\n\n        $this->ensureRootDirectoryExists();\n        $this->ensureDirectoryExists(\n            dirname($destinationPath),\n            $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY))\n        );\n\n        error_clear_last();\n        if ( ! @rename($sourcePath, $destinationPath)) {\n            throw UnableToMoveFile::because(error_get_last()['message'] ?? 'unknown reason', $source, $destination);\n        }\n\n        if ($visibility = $config->get(Config::OPTION_VISIBILITY)) {\n            $this->setVisibility($destination, (string) $visibility);\n        }\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        $sourcePath = $this->prefixer->prefixPath($source);\n        $destinationPath = $this->prefixer->prefixPath($destination);\n        $this->ensureRootDirectoryExists();\n        $this->ensureDirectoryExists(\n            dirname($destinationPath),\n            $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY))\n        );\n\n        error_clear_last();\n        if ($sourcePath !== $destinationPath && ! @copy($sourcePath, $destinationPath)) {\n            throw UnableToCopyFile::because(error_get_last()['message'] ?? 'unknown', $source, $destination);\n        }\n\n        $visibility = $config->get(\n            Config::OPTION_VISIBILITY,\n            $config->get(Config::OPTION_RETAIN_VISIBILITY, true)\n                ? $this->visibility($source)->visibility()\n                : null,\n        );\n\n        if ($visibility) {\n            $this->setVisibility($destination, (string) $visibility);\n        }\n    }\n\n    public function read(string $path): string\n    {\n        $location = $this->prefixer->prefixPath($path);\n        error_clear_last();\n        $contents = @file_get_contents($location);\n\n        if ($contents === false) {\n            throw UnableToReadFile::fromLocation($path, error_get_last()['message'] ?? '');\n        }\n\n        return $contents;\n    }\n\n    public function readStream(string $path)\n    {\n        $location = $this->prefixer->prefixPath($path);\n        error_clear_last();\n        $contents = @fopen($location, 'rb');\n\n        if ($contents === false) {\n            throw UnableToReadFile::fromLocation($path, error_get_last()['message'] ?? '');\n        }\n\n        return $contents;\n    }\n\n    protected function ensureDirectoryExists(string $dirname, int $visibility): void\n    {\n        if (is_dir($dirname)) {\n            return;\n        }\n\n        error_clear_last();\n\n        if ( ! @mkdir($dirname, $visibility, true)) {\n            $mkdirError = error_get_last();\n        }\n\n        clearstatcache(true, $dirname);\n\n        if ( ! is_dir($dirname)) {\n            $errorMessage = isset($mkdirError['message']) ? $mkdirError['message'] : '';\n\n            throw UnableToCreateDirectory::atLocation($dirname, $errorMessage);\n        }\n    }\n\n    public function fileExists(string $location): bool\n    {\n        $location = $this->prefixer->prefixPath($location);\n        clearstatcache();\n        return is_file($location);\n    }\n\n    public function directoryExists(string $location): bool\n    {\n        $location = $this->prefixer->prefixPath($location);\n        clearstatcache();\n        return is_dir($location);\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        $this->ensureRootDirectoryExists();\n        $location = $this->prefixer->prefixPath($path);\n        $visibility = $config->get(Config::OPTION_VISIBILITY, $config->get(Config::OPTION_DIRECTORY_VISIBILITY));\n        $permissions = $this->resolveDirectoryVisibility($visibility);\n\n        if (is_dir($location)) {\n            $this->setPermissions($location, $permissions);\n\n            return;\n        }\n\n        error_clear_last();\n\n        if ( ! @mkdir($location, $permissions, true)) {\n            throw UnableToCreateDirectory::atLocation($path, error_get_last()['message'] ?? '');\n        }\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        $path = $this->prefixer->prefixPath($path);\n        $visibility = is_dir($path) ? $this->visibility->forDirectory($visibility) : $this->visibility->forFile(\n            $visibility\n        );\n\n        $this->setPermissions($path, $visibility);\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        $location = $this->prefixer->prefixPath($path);\n        clearstatcache(false, $location);\n        error_clear_last();\n        $fileperms = @fileperms($location);\n\n        if ($fileperms === false) {\n            throw UnableToRetrieveMetadata::visibility($path, error_get_last()['message'] ?? '');\n        }\n\n        $permissions = $fileperms & 0777;\n        $visibility = $this->visibility->inverseForFile($permissions);\n\n        return new FileAttributes($path, null, $visibility);\n    }\n\n    private function resolveDirectoryVisibility(?string $visibility): int\n    {\n        return $visibility === null ? $this->visibility->defaultForDirectories() : $this->visibility->forDirectory(\n            $visibility\n        );\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        $location = $this->prefixer->prefixPath($path);\n        error_clear_last();\n\n        if ( ! is_file($location)) {\n            throw UnableToRetrieveMetadata::mimeType($location, 'No such file exists.');\n        }\n\n        $mimeType = $this->mimeTypeDetector->detectMimeTypeFromFile($location);\n\n        if ($mimeType === null) {\n            throw UnableToRetrieveMetadata::mimeType($path, error_get_last()['message'] ?? '');\n        }\n\n        return new FileAttributes($path, null, null, null, $mimeType);\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        $location = $this->prefixer->prefixPath($path);\n        clearstatcache();\n        error_clear_last();\n        $lastModified = @filemtime($location);\n\n        if ($lastModified === false) {\n            throw UnableToRetrieveMetadata::lastModified($path, error_get_last()['message'] ?? '');\n        }\n\n        return new FileAttributes($path, null, null, $lastModified);\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        $location = $this->prefixer->prefixPath($path);\n        clearstatcache();\n        error_clear_last();\n\n        if (is_file($location) && ($fileSize = @filesize($location)) !== false) {\n            return new FileAttributes($path, $fileSize);\n        }\n\n        throw UnableToRetrieveMetadata::fileSize($path, error_get_last()['message'] ?? '');\n    }\n\n    public function checksum(string $path, Config $config): string\n    {\n        $algo = $config->get('checksum_algo', 'md5');\n        $location = $this->prefixer->prefixPath($path);\n        error_clear_last();\n        $checksum = @hash_file($algo, $location);\n\n        if ($checksum === false) {\n            throw new UnableToProvideChecksum(error_get_last()['message'] ?? '', $path);\n        }\n\n        return $checksum;\n    }\n\n    private function listDirectory(string $location): Generator\n    {\n        $iterator = new DirectoryIterator($location);\n\n        foreach ($iterator as $item) {\n            if ($item->isDot()) {\n                continue;\n            }\n\n            yield $item;\n        }\n    }\n\n    private function setPermissions(string $location, int $visibility): void\n    {\n        error_clear_last();\n        if ( ! @chmod($location, $visibility)) {\n            $extraMessage = error_get_last()['message'] ?? '';\n            throw UnableToSetVisibility::atLocation($this->prefixer->stripPrefix($location), $extraMessage);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Local/LocalFilesystemAdapterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\Local;\n\nuse const LOCK_EX;\nuse League\\Flysystem\\AdapterTestUtilities\\FilesystemAdapterTestCase;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\Filesystem;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\StorageAttributes;\nuse League\\Flysystem\\SymbolicLinkEncountered;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UnixVisibility\\PortableVisibilityConverter;\nuse League\\Flysystem\\UnixVisibility\\VisibilityConverter;\nuse League\\Flysystem\\Visibility;\nuse League\\MimeTypeDetection\\EmptyExtensionToMimeTypeMap;\nuse League\\MimeTypeDetection\\ExtensionMimeTypeDetector;\nuse League\\MimeTypeDetection\\FinfoMimeTypeDetector;\nuse Traversable;\nuse function file_get_contents;\nuse function file_put_contents;\nuse function fileperms;\nuse function is_resource;\nuse function iterator_to_array;\nuse function mkdir;\nuse function strnatcasecmp;\nuse function symlink;\nuse function usort;\n\n/**\n * @group local\n */\nclass LocalFilesystemAdapterTest extends FilesystemAdapterTestCase\n{\n    public const ROOT = __DIR__ . '/test-root';\n\n    protected function setUp(): void\n    {\n        reset_function_mocks();\n        delete_directory(static::ROOT);\n    }\n\n    protected function tearDown(): void\n    {\n        reset_function_mocks();\n        delete_directory(static::ROOT);\n    }\n\n    /**\n     * @test\n     */\n    public function creating_a_local_filesystem_creates_a_root_directory(): void\n    {\n        new LocalFilesystemAdapter(static::ROOT);\n        $this->assertDirectoryExists(static::ROOT);\n    }\n\n    /**\n     * @test\n     */\n    public function creating_a_local_filesystem_does_not_create_a_root_directory_when_constructed_with_lazy_root_creation(): void\n    {\n        new LocalFilesystemAdapter(static::ROOT, lazyRootCreation: true);\n        $this->assertDirectoryDoesNotExist(static::ROOT);\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_create_a_root_directory_results_in_an_exception(): void\n    {\n        $this->expectException(UnableToCreateDirectory::class);\n        new LocalFilesystemAdapter('/cannot-create/this-directory/');\n    }\n\n    /**\n     * @test\n     *\n     * @see https://github.com/thephpleague/flysystem/issues/1442\n     */\n    public function falling_back_to_extension_lookup_when_finding_mime_type_of_empty_file(): void\n    {\n        $this->givenWeHaveAnExistingFile('something.csv', '');\n\n        $mimeType = $this->adapter()->mimeType('something.csv');\n\n        self::assertEquals('text/csv', $mimeType->mimeType());\n    }\n\n    /**\n     * @test\n     */\n    public function writing_a_file(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n\n        $adapter->write('/file.txt', 'contents', new Config());\n\n        $this->assertFileExists(static::ROOT . '/file.txt');\n        $contents = file_get_contents(static::ROOT . '/file.txt');\n        $this->assertEquals('contents', $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function writing_a_file_with_a_stream(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $stream = stream_with_contents('contents');\n\n        $adapter->writeStream('/file.txt', $stream, new Config());\n        fclose($stream);\n\n        $this->assertFileExists(static::ROOT . '/file.txt');\n        $contents = file_get_contents(static::ROOT . '/file.txt');\n        $this->assertEquals('contents', $contents);\n    }\n\n    /**\n     * @test\n     *\n     * @see https://github.com/thephpleague/flysystem/issues/1606\n     */\n    public function deleting_a_file_during_contents_listing(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT, visibility: new class() implements VisibilityConverter {\n            private VisibilityConverter $visibility;\n\n            public function __construct()\n            {\n                $this->visibility = new PortableVisibilityConverter();\n            }\n\n            public function forFile(string $visibility): int\n            {\n                return $this->visibility->forFile($visibility);\n            }\n\n            public function forDirectory(string $visibility): int\n            {\n                return $this->visibility->forDirectory($visibility);\n            }\n\n            public function inverseForFile(int $visibility): string\n            {\n                unlink(LocalFilesystemAdapterTest::ROOT . '/file-1.txt');\n\n                return $this->visibility->inverseForFile($visibility);\n            }\n\n            public function inverseForDirectory(int $visibility): string\n            {\n                return $this->visibility->inverseForDirectory($visibility);\n            }\n\n            public function defaultForDirectories(): int\n            {\n                return $this->visibility->defaultForDirectories();\n            }\n        });\n        $filesystem = new Filesystem($adapter);\n\n        $filesystem->write('/file-1.txt', 'something');\n        $listing = $filesystem->listContents('/')->toArray();\n        self::assertCount(0, $listing);\n    }\n\n    /**\n     * @test\n     */\n    public function writing_a_file_with_a_stream_and_visibility(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $stream = stream_with_contents('something');\n\n        $adapter->writeStream('/file.txt', $stream, new Config(['visibility' => Visibility::PRIVATE]));\n        fclose($stream);\n\n        $this->assertFileContains(static::ROOT . '/file.txt', 'something');\n        $this->assertFileHasPermissions(static::ROOT . '/file.txt', 0600);\n    }\n\n    /**\n     * @test\n     */\n    public function writing_a_file_with_visibility(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT, new PortableVisibilityConverter());\n        $adapter->write('/file.txt', 'contents', new Config(['visibility' => 'private']));\n        $this->assertFileContains(static::ROOT . '/file.txt', 'contents');\n        $this->assertFileHasPermissions(static::ROOT . '/file.txt', 0600);\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_set_visibility(): void\n    {\n        $this->expectException(UnableToSetVisibility::class);\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->setVisibility('/file.txt', Visibility::PUBLIC);\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_write_a_file(): void\n    {\n        $this->expectException(UnableToWriteFile::class);\n        (new LocalFilesystemAdapter('/'))->write('/cannot-create-a-file-here', 'contents', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_write_a_file_using_a_stream(): void\n    {\n        $this->expectException(UnableToWriteFile::class);\n        try {\n            $stream = stream_with_contents('something');\n            (new LocalFilesystemAdapter('/'))->writeStream('/cannot-create-a-file-here', $stream, new Config());\n        } finally {\n            isset($stream) && is_resource($stream) && fclose($stream);\n        }\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_file(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        file_put_contents(static::ROOT . '/file.txt', 'contents');\n        $adapter->delete('/file.txt');\n        $this->assertFileDoesNotExist(static::ROOT . '/file.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_file_that_does_not_exist(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->delete('/file.txt');\n        $this->assertTrue(true);\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_file_that_cannot_be_deleted(): void\n    {\n        $this->givenWeHaveAnExistingFile('here.txt');\n        mock_function('unlink', false);\n\n        $this->expectException(UnableToDeleteFile::class);\n\n        $this->adapter()->delete('here.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function checking_if_a_file_exists(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->write('/file.txt', 'contents', new Config);\n\n        $this->assertTrue($adapter->fileExists('/file.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function checking_if_a_file_exists_that_does_not_exsist(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n\n        $this->assertFalse($adapter->fileExists('/file.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function listing_contents(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->write('directory/filename.txt', 'content', new Config());\n        $adapter->write('filename.txt', 'content', new Config());\n        /** @var Traversable $contentListing */\n        $contentListing = $adapter->listContents('/', false);\n        $contents = iterator_to_array($contentListing);\n\n        $this->assertCount(2, $contents);\n        $this->assertContainsOnlyInstancesOf(StorageAttributes::class, $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function listing_contents_recursively(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->write('directory/filename.txt', 'content', new Config());\n        $adapter->write('filename.txt', 'content', new Config());\n        /** @var Traversable $contentListing */\n        $contentListing = $adapter->listContents('/', true);\n        $contents = iterator_to_array($contentListing);\n\n        $this->assertCount(3, $contents);\n        $this->assertContainsOnlyInstancesOf(StorageAttributes::class, $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function listing_a_non_existing_directory(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        /** @var Traversable $contentListing */\n        $contentListing = $adapter->listContents('/directory/', false);\n        $contents = iterator_to_array($contentListing);\n\n        $this->assertCount(0, $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function listing_directory_contents_with_link_skipping(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT, null, LOCK_EX, LocalFilesystemAdapter::SKIP_LINKS);\n        $adapter->write('/file.txt', 'content', new Config());\n        symlink(static::ROOT . '/file.txt', static::ROOT . '/link.txt');\n\n        /** @var Traversable $contentListing */\n        $contentListing = $adapter->listContents('/', true);\n        $contents = iterator_to_array($contentListing);\n\n        $this->assertCount(1, $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function listing_directory_contents_with_disallowing_links(): void\n    {\n        $this->expectException(SymbolicLinkEncountered::class);\n        $adapter = new LocalFilesystemAdapter(static::ROOT, null, LOCK_EX, LocalFilesystemAdapter::DISALLOW_LINKS);\n        file_put_contents(static::ROOT . '/file.txt', 'content');\n        symlink(static::ROOT . '/file.txt', static::ROOT . '/link.txt');\n\n        /** @var Traversable $contentListing */\n        $contentListing = $adapter->listContents('/', true);\n        iterator_to_array($contentListing);\n    }\n\n    /**\n     * @test\n     */\n    public function retrieving_visibility_while_listing_directory_contents(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->createDirectory('public', new Config(['visibility' => 'public']));\n        $adapter->createDirectory('private', new Config(['visibility' => 'private']));\n        $adapter->write('public/private.txt', 'private', new Config(['visibility' => 'private']));\n        $adapter->write('private/public.txt', 'public', new Config(['visibility' => 'public']));\n\n        /** @var Traversable<StorageAttributes> $contentListing */\n        $contentListing = $adapter->listContents('/', true);\n        $listing = iterator_to_array($contentListing);\n        usort($listing, function (StorageAttributes $a, StorageAttributes $b) {\n            return strnatcasecmp($a->path(), $b->path());\n        });\n        /**\n         * @var StorageAttributes $publicDirectoryAttributes\n         * @var StorageAttributes $privateFileAttributes\n         * @var StorageAttributes $privateDirectoryAttributes\n         * @var StorageAttributes $publicFileAttributes\n         */\n        [$privateDirectoryAttributes, $publicFileAttributes, $publicDirectoryAttributes, $privateFileAttributes] = $listing;\n\n        $this->assertEquals('public', $publicDirectoryAttributes->visibility());\n        $this->assertEquals('private', $privateFileAttributes->visibility());\n        $this->assertEquals('private', $privateDirectoryAttributes->visibility());\n        $this->assertEquals('public', $publicFileAttributes->visibility());\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_directory(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        mkdir(static::ROOT . '/directory/subdir/', 0744, true);\n        $this->assertDirectoryExists(static::ROOT . '/directory/subdir/');\n        file_put_contents(static::ROOT . '/directory/subdir/file.txt', 'content');\n        symlink(static::ROOT . '/directory/subdir/file.txt', static::ROOT . '/directory/subdir/link.txt');\n        $adapter->deleteDirectory('directory/subdir');\n        $this->assertDirectoryDoesNotExist(static::ROOT . '/directory/subdir/');\n        $adapter->deleteDirectory('directory');\n        $this->assertDirectoryDoesNotExist(static::ROOT . '/directory/');\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_directories_with_other_directories_in_it(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->write('a/b/c/d/e.txt', 'contents', new Config());\n        $adapter->deleteDirectory('a/b');\n        $this->assertDirectoryExists(static::ROOT . '/a');\n        $this->assertDirectoryDoesNotExist(static::ROOT . '/a/b');\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_non_existing_directory(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->deleteDirectory('/non-existing-directory/');\n        $this->assertTrue(true);\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_delete_a_directory(): void\n    {\n        $this->expectException(UnableToDeleteDirectory::class);\n\n        mock_function('rmdir', false);\n\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->createDirectory('/etc/', new Config());\n        $adapter->deleteDirectory('/etc/');\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_delete_a_sub_directory(): void\n    {\n        $this->expectException(UnableToDeleteDirectory::class);\n\n        mock_function('rmdir', false);\n\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->createDirectory('/etc/subdirectory/', new Config());\n        $adapter->deleteDirectory('/etc/');\n    }\n\n    /**\n     * @test\n     */\n    public function creating_a_directory(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->createDirectory('public', new Config(['visibility' => 'public']));\n        $this->assertDirectoryExists(static::ROOT . '/public');\n        $this->assertFileHasPermissions(static::ROOT . '/public', 0755);\n\n        $adapter->createDirectory('private', new Config(['visibility' => 'private']));\n        $this->assertDirectoryExists(static::ROOT . '/private');\n        $this->assertFileHasPermissions(static::ROOT . '/private', 0700);\n\n        $adapter->createDirectory('also_private', new Config(['directory_visibility' => 'private']));\n        $this->assertDirectoryExists(static::ROOT . '/also_private');\n        $this->assertFileHasPermissions(static::ROOT . '/also_private', 0700);\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_create_a_directory(): void\n    {\n        $this->expectException(UnableToCreateDirectory::class);\n        $adapter = new LocalFilesystemAdapter('/');\n        $adapter->createDirectory('/something/', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function creating_a_directory_is_idempotent(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->createDirectory('/something/', new Config(['visibility' => 'private']));\n        $this->assertFileHasPermissions(static::ROOT . '/something', 0700);\n        $adapter->createDirectory('/something/', new Config(['visibility' => 'public']));\n        $this->assertFileHasPermissions(static::ROOT . '/something', 0755);\n    }\n\n    /**\n     * @test\n     */\n    public function retrieving_visibility(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->write('public.txt', 'contents', new Config(['visibility' => 'public']));\n        $this->assertEquals('public', $adapter->visibility('public.txt')->visibility());\n        $adapter->write('private.txt', 'contents', new Config(['visibility' => 'private']));\n        $this->assertEquals('private', $adapter->visibility('private.txt')->visibility());\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_retrieve_visibility(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->visibility('something.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function moving_a_file(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->write('first.txt', 'contents', new Config());\n        $this->assertFileExists(static::ROOT . '/first.txt');\n        $adapter->move('first.txt', 'second.txt', new Config());\n        $this->assertFileExists(static::ROOT . '/second.txt');\n        $this->assertFileDoesNotExist(static::ROOT . '/first.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function moving_a_file_with_visibility(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT, new PortableVisibilityConverter());\n        $adapter->write('first.txt', 'contents', new Config());\n        $this->assertFileExists(static::ROOT . '/first.txt');\n        $this->assertFileHasPermissions(static::ROOT . '/first.txt', 0644);\n        $adapter->move('first.txt', 'second.txt', new Config(['visibility' => 'private']));\n        $this->assertFileExists(static::ROOT . '/second.txt');\n        $this->assertFileHasPermissions(static::ROOT . '/second.txt', 0600);\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_move_a_file(): void\n    {\n        $this->expectException(UnableToMoveFile::class);\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->move('first.txt', 'second.txt', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->write('first.txt', 'contents', new Config());\n        $adapter->copy('first.txt', 'second.txt', new Config());\n        $this->assertFileExists(static::ROOT . '/second.txt');\n        $this->assertFileExists(static::ROOT . '/first.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file_with_visibility(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT, new PortableVisibilityConverter());\n        $adapter->write('first.txt', 'contents', new Config());\n        $adapter->copy('first.txt', 'second.txt', new Config(['visibility' => 'private']));\n        $this->assertFileExists(static::ROOT . '/first.txt');\n        $this->assertFileHasPermissions(static::ROOT . '/first.txt', 0644);\n        $this->assertFileExists(static::ROOT . '/second.txt');\n        $this->assertFileHasPermissions(static::ROOT . '/second.txt', 0600);\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file_retaining_visibility(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT, new PortableVisibilityConverter());\n        $adapter->write('first.txt', 'contents', new Config(['visibility' => 'private']));\n        $adapter->copy('first.txt', 'retain.txt', new Config());\n        $adapter->copy('first.txt', 'do-not-retain.txt', new Config(['retain_visibility' => false]));\n        $this->assertFileExists(static::ROOT . '/first.txt');\n        $this->assertFileHasPermissions(static::ROOT . '/first.txt', 0600);\n        $this->assertFileExists(static::ROOT . '/retain.txt');\n        $this->assertFileHasPermissions(static::ROOT . '/retain.txt', 0600);\n        $this->assertFileExists(static::ROOT . '/do-not-retain.txt');\n        $this->assertFileHasPermissions(static::ROOT . '/do-not-retain.txt', 0644);\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_copy_a_file(): void\n    {\n        $this->expectException(UnableToCopyFile::class);\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->copy('first.txt', 'second.txt', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function getting_mimetype(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->write(\n            'flysystem.svg',\n            (string) file_get_contents(__DIR__ . '/../AdapterTestUtilities/test_files/flysystem.svg'),\n            new Config()\n        );\n        $this->assertStringStartsWith('image/svg+xml', $adapter->mimeType('flysystem.svg')->mimeType());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_get_the_mimetype(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->write(\n            'file.unknown',\n            '',\n            new Config()\n        );\n\n        $this->expectException(UnableToRetrieveMetadata::class);\n\n        $adapter->mimeType('file.unknown');\n    }\n\n    /**\n     * @test\n     */\n    public function allowing_inconclusive_mime_type(): void\n    {\n        $adapter = new LocalFilesystemAdapter(\n            location: static::ROOT,\n            useInconclusiveMimeTypeFallback: true,\n        );\n        $adapter->write(\n            'file.unknown',\n            '',\n            new Config()\n        );\n\n        $this->assertEquals('application/x-empty', $adapter->mimeType('file.unknown')->mimeType());\n    }\n\n    /**\n     * @test\n     */\n    public function fetching_unknown_mime_type_of_a_file(): void\n    {\n        $this->useAdapter(new LocalFilesystemAdapter(self::ROOT, null, LOCK_EX, LocalFilesystemAdapter::DISALLOW_LINKS, new ExtensionMimeTypeDetector(new EmptyExtensionToMimeTypeMap())));\n\n        parent::fetching_unknown_mime_type_of_a_file();\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_get_mimetype(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n        $adapter = new LocalFilesystemAdapter(\n            location: static::ROOT,\n            mimeTypeDetector: new FinfoMimeTypeDetector(),\n        );\n        $adapter->mimeType('flysystem.svg');\n    }\n\n    /**\n     * @test\n     */\n    public function getting_last_modified(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->write('first.txt', 'contents', new Config());\n        mock_function('filemtime', $now = time());\n        $lastModified = $adapter->lastModified('first.txt')->lastModified();\n        $this->assertEquals($now, $lastModified);\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_get_last_modified(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->lastModified('first.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function getting_file_size(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->write('first.txt', 'contents', new Config());\n        $fileSize = $adapter->fileSize('first.txt');\n        $this->assertEquals(8, $fileSize->fileSize());\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_get_file_size(): void\n    {\n        $this->expectException(UnableToRetrieveMetadata::class);\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->fileSize('first.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function reading_a_file(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->write('path.txt', 'contents', new Config());\n        $contents = $adapter->read('path.txt');\n        $this->assertEquals('contents', $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_read_a_file(): void\n    {\n        $this->expectException(UnableToReadFile::class);\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->read('path.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function reading_a_stream(): void\n    {\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->write('path.txt', 'contents', new Config());\n        $contents = $adapter->readStream('path.txt');\n        $this->assertIsResource($contents);\n        $fileContents = stream_get_contents($contents);\n        fclose($contents);\n        $this->assertEquals('contents', $fileContents);\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_stream_read_a_file(): void\n    {\n        $this->expectException(UnableToReadFile::class);\n        $adapter = new LocalFilesystemAdapter(static::ROOT);\n        $adapter->readStream('path.txt');\n    }\n\n    /* //////////////////////\n    // These are the utils //\n    ////////////////////// */\n\n    /**\n     * @param string $file\n     * @param int    $expectedPermissions\n     */\n    private function assertFileHasPermissions(string $file, int $expectedPermissions): void\n    {\n        clearstatcache(false, $file);\n        $permissions = fileperms($file) & 0777;\n        $this->assertEquals($expectedPermissions, $permissions);\n    }\n\n    /**\n     * @param string $file\n     * @param string $expectedContents\n     */\n    private function assertFileContains(string $file, string $expectedContents): void\n    {\n        $this->assertFileExists($file);\n        $contents = file_get_contents($file);\n        $this->assertEquals($expectedContents, $contents);\n    }\n\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        return new LocalFilesystemAdapter(static::ROOT);\n    }\n\n    /**\n     * @test\n     */\n    public function get_checksum_with_specified_algo(): void\n    {\n        /** @var LocalFilesystemAdapter $adapter */\n        $adapter = $this->adapter();\n\n        $adapter->write('path.txt', 'foobar', new Config());\n        $checksum = $adapter->checksum('path.txt', new Config(['checksum_algo' => 'crc32c']));\n\n        $this->assertSame('0d5f5c7f', $checksum);\n    }\n}\n"
  },
  {
    "path": "src/Local/README.md",
    "content": "## Sub-split of Flysystem for local file storage.\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n\n```bash\ncomposer require league/flysystem-local\n```\n\nView the [documentation](https://flysystem.thephpleague.com/docs/adapter/local/).\n"
  },
  {
    "path": "src/Local/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-local\",\n    \"description\": \"Local filesystem adapter for Flysystem.\",\n    \"keywords\": [\"flysystem\", \"filesystem\", \"local\", \"file\", \"files\"],\n    \"type\": \"library\",\n    \"prefer-stable\": true,\n    \"autoload\": {\n        \"psr-4\": {\n            \"League\\\\Flysystem\\\\Local\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"ext-fileinfo\": \"*\",\n        \"league/flysystem\": \"^3.0.0\",\n        \"league/mime-type-detection\": \"^1.0.0\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ]\n}\n"
  },
  {
    "path": "src/MountManager.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse DateTimeInterface;\nuse Throwable;\n\nuse function compact;\nuse function method_exists;\nuse function sprintf;\n\nclass MountManager implements FilesystemOperator\n{\n    /**\n     * @var array<string, FilesystemOperator>\n     */\n    private $filesystems = [];\n\n    /**\n     * @var Config\n     */\n    private $config;\n\n    /**\n     * MountManager constructor.\n     *\n     * @param array<string,FilesystemOperator> $filesystems\n     */\n    public function __construct(array $filesystems = [], array $config = [])\n    {\n        $this->mountFilesystems($filesystems);\n        $this->config = new Config($config);\n    }\n\n    /**\n     * It is not recommended to mount filesystems after creation because interacting\n     * with the Mount Manager becomes unpredictable. Use this as an escape hatch.\n     */\n    public function dangerouslyMountFilesystems(string $key, FilesystemOperator $filesystem): void\n    {\n        $this->mountFilesystem($key, $filesystem);\n    }\n\n    /**\n     * @param array<string,FilesystemOperator> $filesystems\n     */\n    public function extend(array $filesystems, array $config = []): MountManager\n    {\n        $clone = clone $this;\n        $clone->config = $this->config->extend($config);\n        $clone->mountFilesystems($filesystems);\n\n        return $clone;\n    }\n\n    public function fileExists(string $location): bool\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($location);\n\n        try {\n            return $filesystem->fileExists($path);\n        } catch (Throwable $exception) {\n            throw UnableToCheckFileExistence::forLocation($location, $exception);\n        }\n    }\n\n    public function has(string $location): bool\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($location);\n\n        try {\n            return $filesystem->fileExists($path) || $filesystem->directoryExists($path);\n        } catch (Throwable $exception) {\n            throw UnableToCheckExistence::forLocation($location, $exception);\n        }\n    }\n\n    public function directoryExists(string $location): bool\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($location);\n\n        try {\n            return $filesystem->directoryExists($path);\n        } catch (Throwable $exception) {\n            throw UnableToCheckDirectoryExistence::forLocation($location, $exception);\n        }\n    }\n\n    public function read(string $location): string\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($location);\n\n        try {\n            return $filesystem->read($path);\n        } catch (UnableToReadFile $exception) {\n            throw UnableToReadFile::fromLocation($location, $exception->reason(), $exception);\n        }\n    }\n\n    public function readStream(string $location)\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($location);\n\n        try {\n            return $filesystem->readStream($path);\n        } catch (UnableToReadFile $exception) {\n            throw UnableToReadFile::fromLocation($location, $exception->reason(), $exception);\n        }\n    }\n\n    public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path, $mountIdentifier] = $this->determineFilesystemAndPath($location);\n\n        return\n            $filesystem\n                ->listContents($path, $deep)\n                ->map(\n                    function (StorageAttributes $attributes) use ($mountIdentifier) {\n                        return $attributes->withPath(sprintf('%s://%s', $mountIdentifier, $attributes->path()));\n                    }\n                );\n    }\n\n    public function lastModified(string $location): int\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($location);\n\n        try {\n            return $filesystem->lastModified($path);\n        } catch (UnableToRetrieveMetadata $exception) {\n            throw UnableToRetrieveMetadata::lastModified($location, $exception->reason(), $exception);\n        }\n    }\n\n    public function fileSize(string $location): int\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($location);\n\n        try {\n            return $filesystem->fileSize($path);\n        } catch (UnableToRetrieveMetadata $exception) {\n            throw UnableToRetrieveMetadata::fileSize($location, $exception->reason(), $exception);\n        }\n    }\n\n    public function mimeType(string $location): string\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($location);\n\n        try {\n            return $filesystem->mimeType($path);\n        } catch (UnableToRetrieveMetadata $exception) {\n            throw UnableToRetrieveMetadata::mimeType($location, $exception->reason(), $exception);\n        }\n    }\n\n    public function visibility(string $path): string\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $location] = $this->determineFilesystemAndPath($path);\n\n        try {\n            return $filesystem->visibility($location);\n        } catch (UnableToRetrieveMetadata $exception) {\n            throw UnableToRetrieveMetadata::visibility($path, $exception->reason(), $exception);\n        }\n    }\n\n    public function write(string $location, string $contents, array $config = []): void\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($location);\n\n        try {\n            $filesystem->write($path, $contents, $this->config->extend($config)->toArray());\n        } catch (UnableToWriteFile $exception) {\n            throw UnableToWriteFile::atLocation($location, $exception->reason(), $exception);\n        }\n    }\n\n    public function writeStream(string $location, $contents, array $config = []): void\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($location);\n        $filesystem->writeStream($path, $contents, $this->config->extend($config)->toArray());\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($path);\n        $filesystem->setVisibility($path, $visibility);\n    }\n\n    public function delete(string $location): void\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($location);\n\n        try {\n            $filesystem->delete($path);\n        } catch (UnableToDeleteFile $exception) {\n            throw UnableToDeleteFile::atLocation($location, $exception->reason(), $exception);\n        }\n    }\n\n    public function deleteDirectory(string $location): void\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($location);\n\n        try {\n            $filesystem->deleteDirectory($path);\n        } catch (UnableToDeleteDirectory $exception) {\n            throw UnableToDeleteDirectory::atLocation($location, $exception->reason(), $exception);\n        }\n    }\n\n    public function createDirectory(string $location, array $config = []): void\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($location);\n\n        try {\n            $filesystem->createDirectory($path, $this->config->extend($config)->toArray());\n        } catch (UnableToCreateDirectory $exception) {\n            throw UnableToCreateDirectory::dueToFailure($location, $exception);\n        }\n    }\n\n    public function move(string $source, string $destination, array $config = []): void\n    {\n        /** @var FilesystemOperator $sourceFilesystem */\n        /* @var FilesystemOperator $destinationFilesystem */\n        [$sourceFilesystem, $sourcePath] = $this->determineFilesystemAndPath($source);\n        [$destinationFilesystem, $destinationPath] = $this->determineFilesystemAndPath($destination);\n\n        $sourceFilesystem === $destinationFilesystem ? $this->moveInTheSameFilesystem(\n            $sourceFilesystem,\n            $sourcePath,\n            $destinationPath,\n            $source,\n            $destination,\n            $config,\n        ) : $this->moveAcrossFilesystems($source, $destination, $config);\n    }\n\n    public function copy(string $source, string $destination, array $config = []): void\n    {\n        /** @var FilesystemOperator $sourceFilesystem */\n        /* @var FilesystemOperator $destinationFilesystem */\n        [$sourceFilesystem, $sourcePath] = $this->determineFilesystemAndPath($source);\n        [$destinationFilesystem, $destinationPath] = $this->determineFilesystemAndPath($destination);\n\n        $sourceFilesystem === $destinationFilesystem ? $this->copyInSameFilesystem(\n            $sourceFilesystem,\n            $sourcePath,\n            $destinationPath,\n            $source,\n            $destination,\n            $config,\n        ) : $this->copyAcrossFilesystem(\n            $sourceFilesystem,\n            $sourcePath,\n            $destinationFilesystem,\n            $destinationPath,\n            $source,\n            $destination,\n            $config,\n        );\n    }\n\n    public function publicUrl(string $path, array $config = []): string\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($path);\n\n        if ( ! method_exists($filesystem, 'publicUrl')) {\n            throw new UnableToGeneratePublicUrl(sprintf('%s does not support generating public urls.', $filesystem::class), $path);\n        }\n\n        return $filesystem->publicUrl($path, $config);\n    }\n\n    public function temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []): string\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($path);\n\n        if ( ! method_exists($filesystem, 'temporaryUrl')) {\n            throw new UnableToGenerateTemporaryUrl(sprintf('%s does not support generating public urls.', $filesystem::class), $path);\n        }\n\n        return $filesystem->temporaryUrl($path, $expiresAt, $this->config->extend($config)->toArray());\n    }\n\n    public function checksum(string $path, array $config = []): string\n    {\n        /** @var FilesystemOperator $filesystem */\n        [$filesystem, $path] = $this->determineFilesystemAndPath($path);\n\n        if ( ! method_exists($filesystem, 'checksum')) {\n            throw new UnableToProvideChecksum(sprintf('%s does not support providing checksums.', $filesystem::class), $path);\n        }\n\n        return $filesystem->checksum($path, $this->config->extend($config)->toArray());\n    }\n\n    private function mountFilesystems(array $filesystems): void\n    {\n        foreach ($filesystems as $key => $filesystem) {\n            $this->guardAgainstInvalidMount($key, $filesystem);\n            /* @var string $key */\n            /* @var FilesystemOperator $filesystem */\n            $this->mountFilesystem($key, $filesystem);\n        }\n    }\n\n    private function guardAgainstInvalidMount(mixed $key, mixed $filesystem): void\n    {\n        if ( ! is_string($key)) {\n            throw UnableToMountFilesystem::becauseTheKeyIsNotValid($key);\n        }\n\n        if ( ! $filesystem instanceof FilesystemOperator) {\n            throw UnableToMountFilesystem::becauseTheFilesystemWasNotValid($filesystem);\n        }\n    }\n\n    private function mountFilesystem(string $key, FilesystemOperator $filesystem): void\n    {\n        $this->filesystems[$key] = $filesystem;\n    }\n\n    /**\n     * @param string $path\n     *\n     * @return array{0:FilesystemOperator, 1:string, 2:string}\n     */\n    private function determineFilesystemAndPath(string $path): array\n    {\n        if (strpos($path, '://') < 1) {\n            throw UnableToResolveFilesystemMount::becauseTheSeparatorIsMissing($path);\n        }\n\n        /** @var string $mountIdentifier */\n        /** @var string $mountPath */\n        [$mountIdentifier, $mountPath] = explode('://', $path, 2);\n\n        if ( ! array_key_exists($mountIdentifier, $this->filesystems)) {\n            throw UnableToResolveFilesystemMount::becauseTheMountWasNotRegistered($mountIdentifier);\n        }\n\n        return [$this->filesystems[$mountIdentifier], $mountPath, $mountIdentifier];\n    }\n\n    private function copyInSameFilesystem(\n        FilesystemOperator $sourceFilesystem,\n        string $sourcePath,\n        string $destinationPath,\n        string $source,\n        string $destination,\n        array $config,\n    ): void {\n        try {\n            $sourceFilesystem->copy($sourcePath, $destinationPath, $this->config->extend($config)->toArray());\n        } catch (UnableToCopyFile $exception) {\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    private function copyAcrossFilesystem(\n        FilesystemOperator $sourceFilesystem,\n        string $sourcePath,\n        FilesystemOperator $destinationFilesystem,\n        string $destinationPath,\n        string $source,\n        string $destination,\n        array $config,\n    ): void {\n        $config = $this->config->extend($config);\n        $retainVisibility = (bool) $config->get(Config::OPTION_RETAIN_VISIBILITY, true);\n        $visibility = $config->get(Config::OPTION_VISIBILITY);\n\n        try {\n            if ($visibility == null && $retainVisibility) {\n                $visibility = $sourceFilesystem->visibility($sourcePath);\n                $config = $config->extend(compact('visibility'));\n            }\n\n            $stream = $sourceFilesystem->readStream($sourcePath);\n            $destinationFilesystem->writeStream($destinationPath, $stream, $config->toArray());\n        } catch (UnableToRetrieveMetadata | UnableToReadFile | UnableToWriteFile $exception) {\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    private function moveInTheSameFilesystem(\n        FilesystemOperator $sourceFilesystem,\n        string $sourcePath,\n        string $destinationPath,\n        string $source,\n        string $destination,\n        array $config,\n    ): void {\n        try {\n            $sourceFilesystem->move($sourcePath, $destinationPath, $this->config->extend($config)->toArray());\n        } catch (UnableToMoveFile $exception) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    private function moveAcrossFilesystems(string $source, string $destination, array $config = []): void\n    {\n        try {\n            $this->copy($source, $destination, $config);\n            $this->delete($source);\n        } catch (UnableToCopyFile | UnableToDeleteFile $exception) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n}\n"
  },
  {
    "path": "src/MountManagerTest.php",
    "content": "<?php\n\nnamespace League\\Flysystem;\n\nuse League\\Flysystem\\AdapterTestUtilities\\ExceptionThrowingFilesystemAdapter;\nuse League\\Flysystem\\InMemory\\InMemoryFilesystemAdapter;\nuse PHPUnit\\Framework\\TestCase;\n\nuse function fclose;\nuse function is_resource;\nuse function stream_get_contents;\nuse function tmpfile;\n\n/**\n * @group core\n */\nclass MountManagerTest extends TestCase\n{\n    /**\n     * @var ExceptionThrowingFilesystemAdapter\n     */\n    private $firstStubAdapter;\n\n    /**\n     * @var ExceptionThrowingFilesystemAdapter\n     */\n    private $secondStubAdapter;\n\n    /**\n     * @var MountManager\n     */\n    private $mountManager;\n\n    /**\n     * @var Filesystem\n     */\n    private $firstFilesystem;\n\n    /**\n     * @var Filesystem\n     */\n    private $secondFilesystem;\n\n    protected function setUp(): void\n    {\n        $firstFilesystemAdapter = new InMemoryFilesystemAdapter();\n        $secondFilesystemAdapter = new InMemoryFilesystemAdapter();\n        $this->firstStubAdapter = new ExceptionThrowingFilesystemAdapter($firstFilesystemAdapter);\n        $this->secondStubAdapter = new ExceptionThrowingFilesystemAdapter($secondFilesystemAdapter);\n\n        $this->mountManager = new MountManager([\n            'first' => $this->firstFilesystem = new Filesystem($this->firstStubAdapter),\n            'second' => $this->secondFilesystem = new Filesystem($this->secondStubAdapter),\n       ]);\n    }\n\n    /**\n     * @test\n     */\n    public function copying_without_retaining_visibility(): void\n    {\n        // arrange\n        $firstFilesystemAdapter = new InMemoryFilesystemAdapter();\n        $secondFilesystemAdapter = new InMemoryFilesystemAdapter();\n        $mountManager = new MountManager([\n            'first' => new Filesystem($firstFilesystemAdapter, ['visibility' => 'public']),\n            'second' => new Filesystem($secondFilesystemAdapter, ['visibility' => 'private']),\n        ], ['retain_visibility' => false]);\n\n        // act\n        $mountManager->write('first://file.txt', 'contents');\n        $mountManager->copy('first://file.txt', 'second://file.txt');\n\n        // assert\n        $visibility = $mountManager->visibility('second://file.txt');\n        self::assertEquals('private', $visibility);\n    }\n\n    /**\n     * @test\n     */\n    public function extending_without_new_mounts_is_equal_but_not_the_same(): void\n    {\n        $mountManager = $this->mountManager->extend([]);\n\n        $this->assertNotSame($this->mountManager, $mountManager);\n        $this->assertEquals($this->mountManager, $mountManager);\n    }\n\n    /**\n     * @test\n     */\n    public function extending_with_new_mounts_is_not_equal(): void\n    {\n        $mountManager = $this->mountManager->extend([\n            'third' => new Filesystem(new InMemoryFilesystemAdapter()),\n        ]);\n\n        $this->assertNotEquals($this->mountManager, $mountManager);\n    }\n\n    /**\n     * @test\n     */\n    public function extending_exposes_a_usable_mount_on_the_extension(): void\n    {\n        $mountManager = $this->mountManager->extend([\n            'third' => new Filesystem(new InMemoryFilesystemAdapter()),\n        ]);\n\n        $mountManager->write('third://path.txt', 'this');\n        $contents = $mountManager->read('third://path.txt');\n\n        $this->assertEquals('this', $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function extending_does_not_mount_on_the_original_mount_manager(): void\n    {\n        $this->mountManager->extend([\n            'third' => new Filesystem(new InMemoryFilesystemAdapter()),\n        ]);\n\n        $this->expectException(UnableToResolveFilesystemMount::class);\n\n        $this->mountManager->write('third://path.txt', 'this');\n    }\n\n    /**\n     * @test\n     */\n    public function copying_while_retaining_visibility(): void\n    {\n        // arrange\n        $firstFilesystemAdapter = new InMemoryFilesystemAdapter();\n        $secondFilesystemAdapter = new InMemoryFilesystemAdapter();\n        $mountManager = new MountManager([\n            'first' => new Filesystem($firstFilesystemAdapter, ['visibility' => 'public']),\n            'second' => new Filesystem($secondFilesystemAdapter, ['visibility' => 'private']),\n        ], ['retain_visibility' => true]);\n\n        // act\n        $mountManager->write('first://file.txt', 'contents');\n        $mountManager->copy('first://file.txt', 'second://file.txt');\n\n        // assert\n        $visibility = $mountManager->visibility('second://file.txt');\n        self::assertEquals('public', $visibility);\n    }\n\n    /**\n     * @test\n     */\n    public function writing_a_file(): void\n    {\n        $this->mountManager->write('first://file.txt', 'content');\n        $this->mountManager->write('second://another-file.txt', 'content');\n\n        $this->assertTrue($this->firstFilesystem->fileExists('file.txt'));\n        $this->assertFalse($this->secondFilesystem->fileExists('file.txt'));\n\n        $this->assertFalse($this->firstFilesystem->fileExists('another-file.txt'));\n        $this->assertTrue($this->secondFilesystem->fileExists('another-file.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function writing_a_file_with_a_stream(): void\n    {\n        $stream = stream_with_contents('contents');\n        $this->mountManager->writeStream('first://location.txt', $stream);\n\n        $this->assertTrue($this->firstFilesystem->fileExists('location.txt'));\n        $this->assertEquals('contents', $this->firstFilesystem->read('location.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_write_a_file(): void\n    {\n        $this->firstStubAdapter->stageException('write', 'file.txt', UnableToWriteFile::atLocation('file.txt'));\n\n        $this->expectException(UnableToWriteFile::class);\n\n        $this->mountManager->write('first://file.txt', 'content');\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_stream_write_a_file(): void\n    {\n        $handle = tmpfile();\n        $this->firstStubAdapter->stageException('writeStream', 'file.txt', UnableToWriteFile::atLocation('file.txt'));\n\n        $this->expectException(UnableToWriteFile::class);\n\n        try {\n            $this->mountManager->writeStream('first://file.txt', $handle);\n        } finally {\n            is_resource($handle) && fclose($handle);\n        }\n    }\n\n    /**\n     * @description This test method is so ugly, but I don't have the energy to create a nice test for every single one of these method.\n     *\n     * @test\n     *\n     * @dataProvider dpMetadataRetrieverMethods\n     */\n    public function failing_a_one_param_method(string $method, FilesystemOperationFailed $exception): void\n    {\n        $this->firstStubAdapter->stageException($method, 'location.txt', $exception);\n\n        $this->expectException(get_class($exception));\n\n        $this->mountManager->{$method}('first://location.txt');\n    }\n\n    public static function dpMetadataRetrieverMethods(): iterable\n    {\n        yield 'mimeType' => ['mimeType', UnableToRetrieveMetadata::mimeType('location.txt')];\n        yield 'fileSize' => ['fileSize', UnableToRetrieveMetadata::fileSize('location.txt')];\n        yield 'lastModified' => ['lastModified', UnableToRetrieveMetadata::lastModified('location.txt')];\n        yield 'visibility' => ['visibility', UnableToRetrieveMetadata::visibility('location.txt')];\n        yield 'delete' => ['delete', UnableToDeleteFile::atLocation('location.txt')];\n        yield 'deleteDirectory' => ['deleteDirectory', UnableToDeleteDirectory::atLocation('location.txt')];\n        yield 'createDirectory' => ['createDirectory', UnableToCreateDirectory::atLocation('location.txt')];\n        yield 'read' => ['read', UnableToReadFile::fromLocation('location.txt')];\n        yield 'readStream' => ['readStream', UnableToReadFile::fromLocation('location.txt')];\n        yield 'fileExists' => ['fileExists', UnableToCheckFileExistence::forLocation('location.txt')];\n    }\n\n    /**\n     * @test\n     */\n    public function reading_a_file(): void\n    {\n        $this->secondFilesystem->write('location.txt', 'contents');\n\n        $contents = $this->mountManager->read('second://location.txt');\n\n        $this->assertEquals('contents', $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function reading_a_file_as_a_stream(): void\n    {\n        $this->secondFilesystem->write('location.txt', 'contents');\n\n        $handle = $this->mountManager->readStream('second://location.txt');\n        $contents = stream_get_contents($handle);\n        fclose($handle);\n\n        $this->assertEquals('contents', $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function checking_existence_for_an_existing_file(): void\n    {\n        $this->secondFilesystem->write('location.txt', 'contents');\n\n        $existence = $this->mountManager->fileExists('second://location.txt');\n\n        $this->assertTrue($existence);\n    }\n\n    /**\n     * @test\n     */\n    public function checking_existence_for_an_non_existing_file(): void\n    {\n        $existence = $this->mountManager->fileExists('second://location.txt');\n\n        $this->assertFalse($existence);\n    }\n\n    /**\n     * @test\n     */\n    public function checking_existence_for_an_non_existing_directory(): void\n    {\n        $existence = $this->mountManager->directoryExists('second://some-directory');\n\n        $this->assertFalse($existence);\n    }\n\n    /**\n     * @test\n     */\n    public function checking_existence_for_an_existing_directory(): void\n    {\n        $this->secondFilesystem->write('nested/location.txt', 'contents');\n\n        $existence = $this->mountManager->directoryExists('second://nested');\n\n        $this->assertTrue($existence);\n    }\n\n    /**\n     * @test\n     */\n    public function checking_existence_for_an_existing_file_using_has(): void\n    {\n        $this->secondFilesystem->write('location.txt', 'contents');\n\n        $existence = $this->mountManager->has('second://location.txt');\n\n        $this->assertTrue($existence);\n    }\n\n    /**\n     * @test\n     */\n    public function checking_existence_for_an_non_existing_file_using_has(): void\n    {\n        $existence = $this->mountManager->has('second://location.txt');\n\n        $this->assertFalse($existence);\n    }\n\n    /**\n     * @test\n     */\n    public function checking_existence_for_an_non_existing_directory_using_has(): void\n    {\n        $existence = $this->mountManager->has('second://some-directory');\n\n        $this->assertFalse($existence);\n    }\n\n    /**\n     * @test\n     */\n    public function checking_existence_for_an_existing_directory_using_has(): void\n    {\n        $this->secondFilesystem->write('nested/location.txt', 'contents');\n\n        $existence = $this->mountManager->has('second://nested');\n\n        $this->assertTrue($existence);\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_file(): void\n    {\n        $this->firstFilesystem->write('location.txt', 'contents');\n\n        $this->mountManager->delete('first://location.txt');\n\n        $this->assertFalse($this->firstFilesystem->fileExists('location.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_directory(): void\n    {\n        $this->firstFilesystem->write('dirname/location.txt', 'contents');\n\n        $this->mountManager->deleteDirectory('first://dirname');\n\n        $this->assertFalse($this->firstFilesystem->fileExists('dirname/location.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function setting_visibility(): void\n    {\n        $this->firstFilesystem->write('location.txt', 'contents');\n        $this->firstFilesystem->setVisibility('location.txt', Visibility::PRIVATE);\n\n        $this->mountManager->setVisibility('first://location.txt', Visibility::PUBLIC);\n\n        $this->assertEquals(Visibility::PUBLIC, $this->firstFilesystem->visibility('location.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function retrieving_metadata(): void\n    {\n        $now = time();\n        $this->firstFilesystem->write('location.txt', 'contents');\n\n        $lastModified = $this->mountManager->lastModified('first://location.txt');\n        $fileSize = $this->mountManager->fileSize('first://location.txt');\n        $mimeType = $this->mountManager->mimeType('first://location.txt');\n\n        $this->assertGreaterThanOrEqual($now, $lastModified);\n        $this->assertEquals(8, $fileSize);\n        $this->assertEquals('text/plain', $mimeType);\n    }\n\n    /**\n     * @test\n     */\n    public function creating_a_directory(): void\n    {\n        $this->mountManager->createDirectory('first://directory');\n\n        $directoryListing = $this->firstFilesystem->listContents('/')\n            ->toArray();\n\n        $this->assertCount(1, $directoryListing);\n        /** @var DirectoryAttributes $directory */\n        $directory = $directoryListing[0];\n        $this->assertInstanceOf(DirectoryAttributes::class, $directory);\n        $this->assertEquals('directory', $directory->path());\n    }\n\n    /**\n     * @test\n     */\n    public function list_directory(): void\n    {\n        $this->mountManager->createDirectory('first://directory');\n        $this->mountManager->write('first://directory/file', 'foo');\n\n        $directoryListing = $this->mountManager->listContents('first://', Filesystem::LIST_DEEP)->toArray();\n\n        $this->assertCount(2, $directoryListing);\n\n        /** @var DirectoryAttributes $directory */\n        $directory = $directoryListing[0];\n        $this->assertInstanceOf(DirectoryAttributes::class, $directory);\n        $this->assertEquals('first://directory', $directory->path());\n\n        /** @var FileAttributes $file */\n        $file = $directoryListing[1];\n        $this->assertInstanceOf(FileAttributes::class, $file);\n        $this->assertEquals('first://directory/file', $file->path());\n    }\n\n    /**\n     * @test\n     */\n    public function copying_in_the_same_filesystem(): void\n    {\n        $this->firstFilesystem->write('location.txt', 'contents');\n        $this->assertTrue($this->firstFilesystem->fileExists('location.txt'));\n\n        $this->mountManager->copy('first://location.txt', 'first://new-location.txt');\n\n        $this->assertTrue($this->firstFilesystem->fileExists('location.txt'));\n        $this->assertTrue($this->firstFilesystem->fileExists('new-location.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_copy_in_the_same_filesystem(): void\n    {\n        $this->firstFilesystem->write('location.txt', 'contents');\n        $this->firstStubAdapter->stageException('copy', 'location.txt', UnableToCopyFile::fromLocationTo('a', 'b'));\n\n        $this->expectException(UnableToCopyFile::class);\n\n        $this->mountManager->copy('first://location.txt', 'first://new-location.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_move_in_the_same_filesystem(): void\n    {\n        $this->firstFilesystem->write('location.txt', 'contents');\n        $this->firstStubAdapter->stageException('move', 'location.txt', UnableToMoveFile::fromLocationTo('a', 'b'));\n\n        $this->expectException(UnableToMoveFile::class);\n\n        $this->mountManager->move('first://location.txt', 'first://new-location.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function moving_in_the_same_filesystem(): void\n    {\n        $this->firstFilesystem->write('location.txt', 'contents');\n        $this->assertTrue($this->firstFilesystem->fileExists('location.txt'));\n\n        $this->mountManager->move('first://location.txt', 'first://new-location.txt');\n\n        $this->assertFalse($this->firstFilesystem->fileExists('location.txt'));\n        $this->assertTrue($this->firstFilesystem->fileExists('new-location.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function moving_across_filesystem(): void\n    {\n        $this->firstFilesystem->write('location.txt', 'contents');\n        $this->assertTrue($this->firstFilesystem->fileExists('location.txt'));\n\n        $this->mountManager->move('first://location.txt', 'second://new-location.txt');\n\n        $this->assertFalse($this->firstFilesystem->fileExists('location.txt'));\n        $this->assertTrue($this->secondFilesystem->fileExists('new-location.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_move_across_filesystem(): void\n    {\n        $this->firstFilesystem->write('location.txt', 'contents');\n\n        $this->firstStubAdapter->stageException('visibility', 'location.txt', UnableToRetrieveMetadata::visibility('location.txt'));\n\n        $this->expectException(UnableToMoveFile::class);\n\n        $this->mountManager->move('first://location.txt', 'second://new-location.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_copy_across_filesystem(): void\n    {\n        $this->firstFilesystem->write('location.txt', 'contents');\n\n        $this->firstStubAdapter->stageException('visibility', 'location.txt', UnableToRetrieveMetadata::visibility('location.txt'));\n\n        $this->expectException(UnableToCopyFile::class);\n\n        $this->mountManager->copy('first://location.txt', 'second://new-location.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function listing_contents(): void\n    {\n        $this->firstFilesystem->write('contents.txt', 'file contents');\n        $this->firstFilesystem->write('dirname/contents.txt', 'file contents');\n        $this->secondFilesystem->write('dirname/contents.txt', 'file contents');\n\n        $contents = $this->mountManager->listContents('first://', FilesystemReader::LIST_DEEP)->toArray();\n\n        $this->assertCount(3, $contents);\n    }\n\n    /**\n     * @test\n     */\n    public function dangerously_mounting_additional_filesystems(): void\n    {\n        $this->firstFilesystem->write('contents.txt', 'file contents');\n\n        $this->mountManager->dangerouslyMountFilesystems('unknown', $this->firstFilesystem);\n\n        $this->assertTrue($this->mountManager->fileExists('unknown://contents.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function guarding_against_valid_mount_identifiers(): void\n    {\n        $this->expectException(UnableToMountFilesystem::class);\n\n        /* @phpstan-ignore-next-line */\n        new MountManager([1 => new Filesystem(new InMemoryFilesystemAdapter())]);\n    }\n\n    /**\n     * @test\n     */\n    public function guarding_against_mounting_invalid_filesystems(): void\n    {\n        $this->expectException(UnableToMountFilesystem::class);\n\n        /* @phpstan-ignore-next-line */\n        new MountManager(['valid' => 'something else']);\n    }\n\n    /**\n     * @test\n     */\n    public function guarding_against_using_paths_without_mount_prefix(): void\n    {\n        $this->expectException(UnableToResolveFilesystemMount::class);\n\n        $this->mountManager->read('path-without-mount-prefix.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function guard_against_using_unknown_mount(): void\n    {\n        $this->expectException(UnableToResolveFilesystemMount::class);\n\n        $this->mountManager->read('unknown://location.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function generate_public_url(): void\n    {\n        $mountManager = new MountManager([\n            'first' => new Filesystem($this->firstStubAdapter, ['public_url' => 'first.example.com']),\n            'second' => new Filesystem($this->secondStubAdapter, ['public_url' => 'second.example.com']),\n        ]);\n\n        $mountManager->write('first://file1.txt', 'content');\n        $mountManager->write('second://file2.txt', 'content');\n\n        $this->assertSame('first.example.com/file1.txt', $mountManager->publicUrl('first://file1.txt'));\n        $this->assertSame('second.example.com/file2.txt', $mountManager->publicUrl('second://file2.txt'));\n    }\n\n    /**\n     * @test\n     */\n    public function provide_checksum(): void\n    {\n        $this->mountManager->write('first://file.txt', 'content');\n\n        $this->assertSame('9a0364b9e99bb480dd25e1f0284c8555', $this->mountManager->checksum('first://file.txt'));\n    }\n}\n"
  },
  {
    "path": "src/PathNormalizer.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\ninterface PathNormalizer\n{\n    public function normalizePath(string $path): string;\n}\n"
  },
  {
    "path": "src/PathPrefixer.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse function rtrim;\nuse function strlen;\nuse function substr;\n\nfinal class PathPrefixer\n{\n    private string $prefix = '';\n\n    public function __construct(string $prefix, private string $separator = '/')\n    {\n        $this->prefix = rtrim($prefix, '\\\\/');\n\n        if ($this->prefix !== '' || $prefix === $separator) {\n            $this->prefix .= $separator;\n        }\n    }\n\n    public function prefixPath(string $path): string\n    {\n        return $this->prefix . ltrim($path, '\\\\/');\n    }\n\n    public function stripPrefix(string $path): string\n    {\n        /* @var string */\n        return substr($path, strlen($this->prefix));\n    }\n\n    public function stripDirectoryPrefix(string $path): string\n    {\n        return rtrim($this->stripPrefix($path), '\\\\/');\n    }\n\n    public function prefixDirectoryPath(string $path): string\n    {\n        $prefixedPath = $this->prefixPath(rtrim($path, '\\\\/'));\n\n        if ($prefixedPath === '' || substr($prefixedPath, -1) === $this->separator) {\n            return $prefixedPath;\n        }\n\n        return $prefixedPath . $this->separator;\n    }\n}\n"
  },
  {
    "path": "src/PathPrefixerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse PHPUnit\\Framework\\TestCase;\n\nclass PathPrefixerTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function path_prefixing_with_a_prefix(): void\n    {\n        $prefixer = new PathPrefixer('prefix');\n        $prefixedPath = $prefixer->prefixPath('some/path.txt');\n        $this->assertEquals('prefix/some/path.txt', $prefixedPath);\n    }\n\n    /**\n     * @test\n     */\n    public function path_stripping_with_a_prefix(): void\n    {\n        $prefixer = new PathPrefixer('prefix');\n        $strippedPath = $prefixer->stripPrefix('prefix/some/path.txt');\n        $this->assertEquals('some/path.txt', $strippedPath);\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider dpRootPaths\n     */\n    public function an_absolute_root_path_is_supported(string $rootPath, string $separator, string $path, string $expectedPath): void\n    {\n        $prefixer = new PathPrefixer($rootPath, $separator);\n        $prefixedPath = $prefixer->prefixPath($path);\n        $this->assertEquals($expectedPath, $prefixedPath);\n    }\n\n    public static function dpRootPaths(): iterable\n    {\n        yield \"unix-style root path\" => ['/', '/', 'path.txt', '/path.txt'];\n        yield \"windows-style root path\" => ['\\\\', '\\\\', 'path.txt', '\\\\path.txt'];\n    }\n\n    /**\n     * @test\n     */\n    public function path_stripping_is_reversable(): void\n    {\n        $prefixer = new PathPrefixer('prefix');\n        $strippedPath = $prefixer->stripPrefix('prefix/some/path.txt');\n        $this->assertEquals('prefix/some/path.txt', $prefixer->prefixPath($strippedPath));\n        $prefixedPath = $prefixer->prefixPath('some/path.txt');\n        $this->assertEquals('some/path.txt', $prefixer->stripPrefix($prefixedPath));\n    }\n\n    /**\n     * @test\n     */\n    public function prefixing_without_a_prefix(): void\n    {\n        $prefixer = new PathPrefixer('');\n\n        $path = $prefixer->prefixPath('path/to/prefix.txt');\n        $this->assertEquals('path/to/prefix.txt', $path);\n\n        $path = $prefixer->prefixPath('/path/to/prefix.txt');\n        $this->assertEquals('path/to/prefix.txt', $path);\n    }\n\n    /**\n     * @test\n     */\n    public function prefixing_for_a_directory(): void\n    {\n        $prefixer = new PathPrefixer('/prefix');\n\n        $path = $prefixer->prefixDirectoryPath('something');\n        $this->assertEquals('/prefix/something/', $path);\n        $path = $prefixer->prefixDirectoryPath('');\n        $this->assertEquals('/prefix/', $path);\n    }\n\n    /**\n     * @test\n     */\n    public function prefixing_for_a_directory_without_a_prefix(): void\n    {\n        $prefixer = new PathPrefixer('');\n\n        $path = $prefixer->prefixDirectoryPath('something');\n        $this->assertEquals('something/', $path);\n        $path = $prefixer->prefixDirectoryPath('');\n        $this->assertEquals('', $path);\n    }\n\n    /**\n     * @test\n     */\n    public function stripping_a_directory_prefix(): void\n    {\n        $prefixer = new PathPrefixer('/something/');\n\n        $path = $prefixer->stripDirectoryPrefix('/something/this/');\n        $this->assertEquals('this', $path);\n        $path = $prefixer->stripDirectoryPrefix('/something/and-this\\\\');\n        $this->assertEquals('and-this', $path);\n    }\n}\n"
  },
  {
    "path": "src/PathPrefixing/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n**/*Test.php export-ignore\nREADME.md export-ignore\n"
  },
  {
    "path": "src/PathPrefixing/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/PathPrefixing/LICENSE",
    "content": "Copyright (c) 2013-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/PathPrefixing/PathPrefixedAdapter.php",
    "content": "<?php\n\nnamespace League\\Flysystem\\PathPrefixing;\n\nuse DateTimeInterface;\nuse Generator;\nuse League\\Flysystem\\CalculateChecksumFromStream;\nuse League\\Flysystem\\ChecksumProvider;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\UnableToCheckDirectoryExistence;\nuse League\\Flysystem\\UnableToCheckFileExistence;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToGeneratePublicUrl;\nuse League\\Flysystem\\UnableToGenerateTemporaryUrl;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UrlGeneration\\PublicUrlGenerator;\nuse League\\Flysystem\\UrlGeneration\\TemporaryUrlGenerator;\nuse Throwable;\n\nclass PathPrefixedAdapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider, TemporaryUrlGenerator\n{\n    use CalculateChecksumFromStream;\n\n    private PathPrefixer $prefix;\n\n    public function __construct(private FilesystemAdapter $adapter, string $prefix)\n    {\n        if ($prefix === '') {\n            throw new \\InvalidArgumentException('The prefix must not be empty.');\n        }\n\n        $this->prefix = new PathPrefixer($prefix);\n    }\n\n    public function read(string $location): string\n    {\n        try {\n            return $this->adapter->read($this->prefix->prefixPath($location));\n        } catch (Throwable $previous) {\n            throw UnableToReadFile::fromLocation($location, $previous->getMessage(), $previous);\n        }\n    }\n\n    public function readStream(string $location)\n    {\n        try {\n            return $this->adapter->readStream($this->prefix->prefixPath($location));\n        } catch (Throwable $previous) {\n            throw UnableToReadFile::fromLocation($location, $previous->getMessage(), $previous);\n        }\n    }\n\n    public function listContents(string $location, bool $deep): Generator\n    {\n        foreach ($this->adapter->listContents($this->prefix->prefixPath($location), $deep) as $attributes) {\n            yield $attributes->withPath($this->prefix->stripPrefix($attributes->path()));\n        }\n    }\n\n    public function fileExists(string $location): bool\n    {\n        try {\n            return $this->adapter->fileExists($this->prefix->prefixPath($location));\n        } catch (Throwable $previous) {\n            throw UnableToCheckFileExistence::forLocation($location, $previous);\n        }\n    }\n\n    public function directoryExists(string $location): bool\n    {\n        try {\n            return $this->adapter->directoryExists($this->prefix->prefixPath($location));\n        } catch (Throwable $previous) {\n            throw UnableToCheckDirectoryExistence::forLocation($location, $previous);\n        }\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        try {\n            return $this->adapter->lastModified($this->prefix->prefixPath($path));\n        } catch (Throwable $previous) {\n            throw UnableToRetrieveMetadata::lastModified($path, $previous->getMessage(), $previous);\n        }\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        try {\n            return $this->adapter->fileSize($this->prefix->prefixPath($path));\n        } catch (Throwable $previous) {\n            throw UnableToRetrieveMetadata::fileSize($path, $previous->getMessage(), $previous);\n        }\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        try {\n            return $this->adapter->mimeType($this->prefix->prefixPath($path));\n        } catch (Throwable $previous) {\n            throw UnableToRetrieveMetadata::mimeType($path, $previous->getMessage(), $previous);\n        }\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        try {\n            return $this->adapter->visibility($this->prefix->prefixPath($path));\n        } catch (Throwable $previous) {\n            throw UnableToRetrieveMetadata::visibility($path, $previous->getMessage(), $previous);\n        }\n    }\n\n    public function write(string $location, string $contents, Config $config): void\n    {\n        try {\n            $this->adapter->write($this->prefix->prefixPath($location), $contents, $config);\n        } catch (Throwable $previous) {\n            throw UnableToWriteFile::atLocation($location, $previous->getMessage(), $previous);\n        }\n    }\n\n    public function writeStream(string $location, $contents, Config $config): void\n    {\n        try {\n            $this->adapter->writeStream($this->prefix->prefixPath($location), $contents, $config);\n        } catch (Throwable $previous) {\n            throw UnableToWriteFile::atLocation($location, $previous->getMessage(), $previous);\n        }\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        try {\n            $this->adapter->setVisibility($this->prefix->prefixPath($path), $visibility);\n        } catch (Throwable $previous) {\n            throw UnableToSetVisibility::atLocation($path, $previous->getMessage(), $previous);\n        }\n    }\n\n    public function delete(string $location): void\n    {\n        try {\n            $this->adapter->delete($this->prefix->prefixPath($location));\n        } catch (Throwable $previous) {\n            throw UnableToDeleteFile::atLocation($location, $previous->getMessage(), $previous);\n        }\n    }\n\n    public function deleteDirectory(string $location): void\n    {\n        try {\n            $this->adapter->deleteDirectory($this->prefix->prefixPath($location));\n        } catch (Throwable $previous) {\n            throw UnableToDeleteDirectory::atLocation($location, $previous->getMessage(), $previous);\n        }\n    }\n\n    public function createDirectory(string $location, Config $config): void\n    {\n        try {\n            $this->adapter->createDirectory($this->prefix->prefixPath($location), $config);\n        } catch (Throwable $previous) {\n            throw UnableToCreateDirectory::atLocation($location, $previous->getMessage(), $previous);\n        }\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        try {\n            $this->adapter->move($this->prefix->prefixPath($source), $this->prefix->prefixPath($destination), $config);\n        } catch (Throwable $previous) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $previous);\n        }\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        try {\n            $this->adapter->copy($this->prefix->prefixPath($source), $this->prefix->prefixPath($destination), $config);\n        } catch (Throwable $previous) {\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $previous);\n        }\n    }\n\n    public function publicUrl(string $path, Config $config): string\n    {\n        if ( ! $this->adapter instanceof PublicUrlGenerator) {\n            throw UnableToGeneratePublicUrl::noGeneratorConfigured($path);\n        }\n\n        return $this->adapter->publicUrl($this->prefix->prefixPath($path), $config);\n    }\n\n    public function checksum(string $path, Config $config): string\n    {\n        if ($this->adapter instanceof ChecksumProvider) {\n            return $this->adapter->checksum($this->prefix->prefixPath($path), $config);\n        }\n\n        return $this->calculateChecksumFromStream($path, $config);\n    }\n\n    public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string\n    {\n        if ( ! $this->adapter instanceof TemporaryUrlGenerator) {\n            throw UnableToGenerateTemporaryUrl::noGeneratorConfigured($path);\n        }\n\n        return $this->adapter->temporaryUrl($this->prefix->prefixPath($path), $expiresAt, $config);\n    }\n}\n"
  },
  {
    "path": "src/PathPrefixing/PathPrefixedAdapterTest.php",
    "content": "<?php\n\nnamespace League\\Flysystem\\PathPrefixing;\n\nuse League\\Flysystem\\ChecksumProvider;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\InMemory\\InMemoryFilesystemAdapter;\nuse League\\Flysystem\\UnableToGeneratePublicUrl;\nuse League\\Flysystem\\UrlGeneration\\PublicUrlGenerator;\nuse League\\Flysystem\\Visibility;\nuse PHPUnit\\Framework\\TestCase;\n\nuse function iterator_to_array;\n\nclass PathPrefixedAdapterTest extends TestCase\n{\n    public function testPrefix(): void\n    {\n        $adapter = new InMemoryFilesystemAdapter();\n        $prefix = new PathPrefixedAdapter($adapter, 'foo');\n\n        $prefix->write('foo.txt', 'bla', new Config);\n        static::assertTrue($prefix->fileExists('foo.txt'));\n        static::assertFalse($prefix->directoryExists('foo.txt'));\n        static::assertTrue($adapter->fileExists('foo/foo.txt'));\n        static::assertFalse($adapter->directoryExists('foo/foo.txt'));\n\n        static::assertSame('bla', $prefix->read('foo.txt'));\n        static::assertSame('bla', stream_get_contents($prefix->readStream('foo.txt')));\n        static::assertSame('text/plain', $prefix->mimeType('foo.txt')->mimeType());\n        static::assertSame(3, $prefix->fileSize('foo.txt')->fileSize());\n        static::assertSame(Visibility::PUBLIC, $prefix->visibility('foo.txt')->visibility());\n        $prefix->setVisibility('foo.txt', Visibility::PRIVATE);\n        static::assertSame(Visibility::PRIVATE, $prefix->visibility('foo.txt')->visibility());\n        static::assertEqualsWithDelta($prefix->lastModified('foo.txt')->lastModified(), time(), 2);\n\n        $prefix->copy('foo.txt', 'bla.txt', new Config);\n        static::assertTrue($prefix->fileExists('bla.txt'));\n\n        $prefix->createDirectory('dir', new Config());\n        static::assertTrue($prefix->directoryExists('dir'));\n        static::assertFalse($prefix->directoryExists('dir2'));\n        $prefix->deleteDirectory('dir');\n        static::assertFalse($prefix->directoryExists('dir'));\n\n        $prefix->move('bla.txt', 'bla2.txt', new Config());\n        static::assertFalse($prefix->fileExists('bla.txt'));\n        static::assertTrue($prefix->fileExists('bla2.txt'));\n\n        $prefix->delete('bla2.txt');\n        static::assertFalse($prefix->fileExists('bla2.txt'));\n\n        $prefix->createDirectory('test', new Config());\n\n        $files = iterator_to_array($prefix->listContents('', true));\n        static::assertCount(2, $files);\n    }\n\n    public function testWriteStream(): void\n    {\n        $adapter = new InMemoryFilesystemAdapter();\n        $prefix = new PathPrefixedAdapter($adapter, 'foo');\n        $tmpFile = sys_get_temp_dir() . '/' . uniqid('test', true);\n        file_put_contents($tmpFile, 'test');\n\n        $prefix->writeStream('a.txt', fopen($tmpFile, 'rb'), new Config());\n\n        static::assertTrue($prefix->fileExists('a.txt'));\n        static::assertSame('test', $prefix->read('a.txt'));\n        static::assertSame('test', stream_get_contents($prefix->readStream('a.txt')));\n\n        unlink($tmpFile);\n    }\n\n    public function testEmptyPrefix(): void\n    {\n        static::expectException(\\InvalidArgumentException::class);\n        new PathPrefixedAdapter(new InMemoryFilesystemAdapter(), '');\n    }\n\n    /**\n     * @test\n     */\n    public function generating_a_public_url(): void\n    {\n        $adapter = new class() extends InMemoryFilesystemAdapter implements PublicUrlGenerator {\n            public function publicUrl(string $path, Config $config): string\n            {\n                return 'memory://' . ltrim($path, '/');\n            }\n        };\n        $prefixedAdapter = new PathPrefixedAdapter($adapter, 'prefix');\n\n        $url = $prefixedAdapter->publicUrl('/path.txt', new Config());\n\n        self::assertEquals('memory://prefix/path.txt', $url);\n    }\n\n    /**\n     * @test\n     */\n    public function calculate_checksum_using_decorated_adapter(): void\n    {\n        $adapter = new class() extends InMemoryFilesystemAdapter implements ChecksumProvider {\n            public function checksum(string $path, Config $config): string\n            {\n                return hash('md5', $this->read($path));\n            }\n        };\n\n        $prefixedAdapter = new PathPrefixedAdapter($adapter, 'prefix');\n        $prefixedAdapter->write('foo.txt', 'bla', new Config);\n\n        self::assertEquals('128ecf542a35ac5270a87dc740918404', $prefixedAdapter->checksum('foo.txt', new Config()));\n    }\n\n    /**\n     * @test\n     */\n    public function calculate_checksum_using_current_adapter(): void\n    {\n        $adapter = new InMemoryFilesystemAdapter();\n        $prefixedAdapter = new PathPrefixedAdapter($adapter, 'prefix');\n        $prefixedAdapter->write('foo.txt', 'bla', new Config);\n\n        self::assertEquals('128ecf542a35ac5270a87dc740918404', hash('md5', 'bla'));\n        self::assertEquals('128ecf542a35ac5270a87dc740918404', $prefixedAdapter->checksum('foo.txt', new Config()));\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_generate_a_public_url(): void\n    {\n        $prefixedAdapter = new PathPrefixedAdapter(new InMemoryFilesystemAdapter(), 'prefix');\n\n        $this->expectException(UnableToGeneratePublicUrl::class);\n\n        $prefixedAdapter->publicUrl('/path.txt', new Config());\n    }\n}\n"
  },
  {
    "path": "src/PathPrefixing/README.md",
    "content": "## Sub-split of Flysystem for path prefixed adapter decoration.\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n\n```bash\ncomposer require league/flysystem-path-prefixing\n```\n\nView the [documentation](https://flysystem.thephpleague.com/docs/adapter/path-prefixing/).\n"
  },
  {
    "path": "src/PathPrefixing/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-path-prefixing\",\n    \"description\": \"Path prefixing filesystem adapter for Flysystem.\",\n    \"keywords\": [\"flysystem\", \"filesystem\", \"prefixing\", \"prefix\"],\n    \"type\": \"library\",\n    \"prefer-stable\": true,\n    \"autoload\": {\n        \"psr-4\": {\n            \"League\\\\Flysystem\\\\PathPrefixing\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"league/flysystem\": \"^3.10.0\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ]\n}\n"
  },
  {
    "path": "src/PathTraversalDetected.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\n\nclass PathTraversalDetected extends RuntimeException implements FilesystemException\n{\n    private string $path;\n\n    public function path(): string\n    {\n        return $this->path;\n    }\n\n    public static function forPath(string $path): PathTraversalDetected\n    {\n        $e = new PathTraversalDetected(\"Path traversal detected: {$path}\");\n        $e->path = $path;\n\n        return $e;\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV2/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n**/*Test.php export-ignore\n**/*Stub.php export-ignore\nREADME.md export-ignore\n"
  },
  {
    "path": "src/PhpseclibV2/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/PhpseclibV2/ConnectionProvider.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse phpseclib\\Net\\SFTP;\n\n/**\n * @deprecated The \"League\\Flysystem\\PhpseclibV2\\ConnectionProvider\" class is deprecated since Flysystem 3.0, use \"League\\Flysystem\\PhpseclibV3\\ConnectionProvider\" instead.\n */\ninterface ConnectionProvider\n{\n    public function provideConnection(): SFTP;\n}\n"
  },
  {
    "path": "src/PhpseclibV2/ConnectivityChecker.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse phpseclib\\Net\\SFTP;\n\n/**\n * @deprecated The \"League\\Flysystem\\PhpseclibV2\\ConnectivityChecker\" class is deprecated since Flysystem 3.0, use \"League\\Flysystem\\PhpseclibV3\\ConnectivityChecker\" instead.\n */\ninterface ConnectivityChecker\n{\n    public function isConnected(SFTP $connection): bool;\n}\n"
  },
  {
    "path": "src/PhpseclibV2/FixatedConnectivityChecker.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse phpseclib\\Net\\SFTP;\n\n/**\n * @deprecated The \"League\\Flysystem\\PhpseclibV2\\FixatedConnectivityChecker\" class is deprecated since Flysystem 3.0, use \"League\\Flysystem\\PhpseclibV3\\FixatedConnectivityChecker\" instead.\n */\nclass FixatedConnectivityChecker implements ConnectivityChecker\n{\n    /**\n     * @var int\n     */\n    private $succeedAfter;\n\n    /**\n     * @var int\n     */\n    private $numberOfTimesChecked = 0;\n\n    public function __construct(int $succeedAfter = 0)\n    {\n        $this->succeedAfter = $succeedAfter;\n    }\n\n    public function isConnected(SFTP $connection): bool\n    {\n        if ($this->numberOfTimesChecked >= $this->succeedAfter) {\n            return true;\n        }\n\n        $this->numberOfTimesChecked++;\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV2/README.md",
    "content": "# CAUTION: This package is deprecated since Flysystem 3.0 Instead, use the [Flysystem for SFTP v3](https://github.com/thephpleague/flysystem-sftp-v3)\n\n## Sub-split of Flysystem for SFTP using phpseclib2.\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n\n```bash\ncomposer require league/flysystem-sftp\n```\n\nView the [documentation](https://flysystem.thephpleague.com/docs/adapter/sftp/).\n"
  },
  {
    "path": "src/PhpseclibV2/SftpAdapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\FilesystemException;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\StorageAttributes;\nuse League\\Flysystem\\UnableToCheckDirectoryExistence;\nuse League\\Flysystem\\UnableToCheckFileExistence;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UnixVisibility\\PortableVisibilityConverter;\nuse League\\Flysystem\\UnixVisibility\\VisibilityConverter;\nuse League\\MimeTypeDetection\\FinfoMimeTypeDetector;\nuse League\\MimeTypeDetection\\MimeTypeDetector;\nuse phpseclib\\Net\\SFTP;\nuse Throwable;\n\nuse function rtrim;\n\n/**\n * @deprecated The \"League\\Flysystem\\PhpseclibV2\\SftpAdapter\" class is deprecated since Flysystem 3.0, use \"League\\Flysystem\\PhpseclibV3\\SftpAdapter\" instead.\n */\nclass SftpAdapter implements FilesystemAdapter\n{\n    /**\n     * @var ConnectionProvider\n     */\n    private $connectionProvider;\n\n    /**\n     * @var VisibilityConverter\n     */\n    private $visibilityConverter;\n\n    /**\n     * @var PathPrefixer\n     */\n    private $prefixer;\n\n    /**\n     * @var MimeTypeDetector\n     */\n    private $mimeTypeDetector;\n\n    public function __construct(\n        ConnectionProvider $connectionProvider,\n        string $root,\n        ?VisibilityConverter $visibilityConverter = null,\n        ?MimeTypeDetector $mimeTypeDetector = null,\n        private bool $detectMimeTypeUsingPath = false,\n    ) {\n        $this->connectionProvider = $connectionProvider;\n        $this->prefixer = new PathPrefixer($root);\n        $this->visibilityConverter = $visibilityConverter ?? new PortableVisibilityConverter();\n        $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();\n    }\n\n    public function fileExists(string $path): bool\n    {\n        $location = $this->prefixer->prefixPath($path);\n\n        try {\n            return $this->connectionProvider->provideConnection()->is_file($location);\n        } catch (Throwable $exception) {\n            throw UnableToCheckFileExistence::forLocation($path, $exception);\n        }\n    }\n\n    public function directoryExists(string $path): bool\n    {\n        $location = $this->prefixer->prefixDirectoryPath($path);\n\n        try {\n            return $this->connectionProvider->provideConnection()->is_dir($location);\n        } catch (Throwable $exception) {\n            throw UnableToCheckDirectoryExistence::forLocation($path, $exception);\n        }\n    }\n\n    /**\n     * @param string          $path\n     * @param string|resource $contents\n     * @param Config          $config\n     *\n     * @throws FilesystemException\n     */\n    private function upload(string $path, $contents, Config $config): void\n    {\n        $this->ensureParentDirectoryExists($path, $config);\n        $connection = $this->connectionProvider->provideConnection();\n        $location = $this->prefixer->prefixPath($path);\n\n        if ( ! $connection->put($location, $contents, SFTP::SOURCE_STRING)) {\n            throw UnableToWriteFile::atLocation($path, 'not able to write the file');\n        }\n\n        if ($visibility = $config->get(Config::OPTION_VISIBILITY)) {\n            $this->setVisibility($path, $visibility);\n        }\n    }\n\n    private function ensureParentDirectoryExists(string $path, Config $config): void\n    {\n        $parentDirectory = dirname($path);\n\n        if ($parentDirectory === '' || $parentDirectory === '.') {\n            return;\n        }\n\n        /** @var string $visibility */\n        $visibility = $config->get(Config::OPTION_DIRECTORY_VISIBILITY);\n        $this->makeDirectory($parentDirectory, $visibility);\n    }\n\n    private function makeDirectory(string $directory, ?string $visibility): void\n    {\n        $location = $this->prefixer->prefixPath($directory);\n        $connection = $this->connectionProvider->provideConnection();\n\n        if ($connection->is_dir($location)) {\n            return;\n        }\n\n        $mode = $visibility ? $this->visibilityConverter->forDirectory(\n            $visibility\n        ) : $this->visibilityConverter->defaultForDirectories();\n\n        if ( ! $connection->mkdir($location, $mode, true) && ! $connection->is_dir($location)) {\n            throw UnableToCreateDirectory::atLocation($directory);\n        }\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        try {\n            $this->upload($path, $contents, $config);\n        } catch (UnableToWriteFile $exception) {\n            throw $exception;\n        } catch (Throwable $exception) {\n            throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        try {\n            $this->upload($path, $contents, $config);\n        } catch (UnableToWriteFile $exception) {\n            throw $exception;\n        } catch (Throwable $exception) {\n            throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function read(string $path): string\n    {\n        $location = $this->prefixer->prefixPath($path);\n        $connection = $this->connectionProvider->provideConnection();\n        $contents = $connection->get($location);\n\n        if ( ! is_string($contents)) {\n            throw UnableToReadFile::fromLocation($path);\n        }\n\n        return $contents;\n    }\n\n    public function readStream(string $path)\n    {\n        $location = $this->prefixer->prefixPath($path);\n        $connection = $this->connectionProvider->provideConnection();\n        /** @var resource $readStream */\n        $readStream = fopen('php://temp', 'w+');\n\n        if ( ! $connection->get($location, $readStream)) {\n            fclose($readStream);\n            throw UnableToReadFile::fromLocation($path);\n        }\n\n        rewind($readStream);\n\n        return $readStream;\n    }\n\n    public function delete(string $path): void\n    {\n        $location = $this->prefixer->prefixPath($path);\n        $connection = $this->connectionProvider->provideConnection();\n        $connection->delete($location);\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        $location = rtrim($this->prefixer->prefixPath($path), '/') . '/';\n        $connection = $this->connectionProvider->provideConnection();\n        $connection->delete($location);\n        $connection->rmdir($location);\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        $this->makeDirectory($path, $config->get(Config::OPTION_DIRECTORY_VISIBILITY, $config->get(Config::OPTION_VISIBILITY)));\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        $location = $this->prefixer->prefixPath($path);\n        $connection = $this->connectionProvider->provideConnection();\n        $mode = $this->visibilityConverter->forFile($visibility);\n\n        if ( ! $connection->chmod($mode, $location, false)) {\n            throw UnableToSetVisibility::atLocation($path);\n        }\n    }\n\n    private function fetchFileMetadata(string $path, string $type): FileAttributes\n    {\n        $location = $this->prefixer->prefixPath($path);\n        $connection = $this->connectionProvider->provideConnection();\n        $stat = $connection->stat($location);\n\n        if ( ! is_array($stat)) {\n            throw UnableToRetrieveMetadata::create($path, $type);\n        }\n\n        $attributes = $this->convertListingToAttributes($path, $stat);\n\n        if ( ! $attributes instanceof FileAttributes) {\n            throw UnableToRetrieveMetadata::create($path, $type, 'path is not a file');\n        }\n\n        return $attributes;\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        try {\n            $mimetype = $this->detectMimeTypeUsingPath\n                ? $this->mimeTypeDetector->detectMimeTypeFromPath($path)\n                : $this->mimeTypeDetector->detectMimeType($path, $this->read($path));\n        } catch (Throwable $exception) {\n            throw UnableToRetrieveMetadata::mimeType($path, $exception->getMessage(), $exception);\n        }\n\n        if ($mimetype === null) {\n            throw UnableToRetrieveMetadata::mimeType($path, 'Unknown.');\n        }\n\n        return new FileAttributes($path, null, null, null, $mimetype);\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_LAST_MODIFIED);\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_FILE_SIZE);\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_VISIBILITY);\n    }\n\n    public function listContents(string $path, bool $deep): iterable\n    {\n        $connection = $this->connectionProvider->provideConnection();\n        $location = $this->prefixer->prefixPath(rtrim($path, '/')) . '/';\n        $listing = $connection->rawlist($location, false);\n\n        if ($listing === false) {\n            return;\n        }\n\n        foreach ($listing as $filename => $attributes) {\n            if ($filename === '.' || $filename === '..') {\n                continue;\n            }\n\n            // Ensure numeric keys are strings.\n            $filename = (string) $filename;\n            $path = $this->prefixer->stripPrefix($location . ltrim($filename, '/'));\n            $attributes = $this->convertListingToAttributes($path, $attributes);\n            yield $attributes;\n\n            if ($deep && $attributes->isDir()) {\n                foreach ($this->listContents($attributes->path(), true) as $child) {\n                    yield $child;\n                }\n            }\n        }\n    }\n\n    private function convertListingToAttributes(string $path, array $attributes): StorageAttributes\n    {\n        $permissions = $attributes['permissions'] & 0777;\n        $lastModified = $attributes['mtime'] ?? null;\n\n        if (($attributes['type'] ?? null) === NET_SFTP_TYPE_DIRECTORY) {\n            return new DirectoryAttributes(\n                ltrim($path, '/'),\n                $this->visibilityConverter->inverseForDirectory($permissions),\n                $lastModified\n            );\n        }\n\n        return new FileAttributes(\n            $path,\n            $attributes['size'],\n            $this->visibilityConverter->inverseForFile($permissions),\n            $lastModified\n        );\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        $sourceLocation = $this->prefixer->prefixPath($source);\n        $destinationLocation = $this->prefixer->prefixPath($destination);\n        $connection = $this->connectionProvider->provideConnection();\n\n        try {\n            $this->ensureParentDirectoryExists($destination, $config);\n        } catch (Throwable $exception) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);\n        }\n\n        if ( ! $connection->rename($sourceLocation, $destinationLocation)) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination);\n        }\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        try {\n            $readStream = $this->readStream($source);\n            $visibility = $this->visibility($source)->visibility();\n            $this->writeStream($destination, $readStream, new Config(compact(Config::OPTION_VISIBILITY)));\n        } catch (Throwable $exception) {\n            if (isset($readStream) && is_resource($readStream)) {\n                @fclose($readStream);\n            }\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV2/SftpAdapterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse League\\Flysystem\\AdapterTestUtilities\\FilesystemAdapterTestCase;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToWriteFile;\n\nuse function class_exists;\n\n/**\n * @group sftp\n * @group legacy\n */\nclass SftpAdapterTest extends FilesystemAdapterTestCase\n{\n    /**\n     * @var ConnectionProvider\n     */\n    private static $connectionProvider;\n\n    /**\n     * @var SftpStub\n     */\n    private $connection;\n\n    public static function setUpBeforeClass(): void\n    {\n        if ( ! class_exists('phpseclib\\Net\\SFTP')) {\n            self::markTestSkipped(\"PHPSecLib V2 is not installed\");\n        }\n    }\n\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        return new SftpAdapter(\n            static::connectionProvider(),\n            '/upload'\n        );\n    }\n\n    /**\n     * @before\n     */\n    public function setupConnectionProvider(): void\n    {\n        /** @var SftpStub $connection */\n        $connection = static::connectionProvider()->provideConnection();\n        $this->connection = $connection;\n        $this->connection->reset();\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_create_a_directory(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n\n        $this->expectException(UnableToCreateDirectory::class);\n\n        $adapter->createDirectory('not-gonna-happen', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_write_a_file(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n\n        $this->expectException(UnableToWriteFile::class);\n\n        $adapter->write('not-gonna-happen', 'na-ah', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_read_a_file(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n\n        $this->expectException(UnableToReadFile::class);\n\n        $adapter->read('not-gonna-happen');\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_read_a_file_as_a_stream(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n\n        $this->expectException(UnableToReadFile::class);\n\n        $adapter->readStream('not-gonna-happen');\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_write_a_file_using_streams(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n        $writeHandle = stream_with_contents('contents');\n\n        $this->expectException(UnableToWriteFile::class);\n\n        try {\n            $adapter->writeStream('not-gonna-happen', $writeHandle, new Config());\n        } finally {\n            fclose($writeHandle);\n        }\n    }\n\n    /**\n     * @test\n     */\n    public function detecting_mimetype(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('file.svg', (string) file_get_contents(__DIR__ . '/../AdapterTestUtilities/test_files/flysystem.svg'), new Config());\n\n        $mimeType = $adapter->mimeType('file.svg');\n\n        $this->assertStringStartsWith('image/svg+xml', $mimeType->mimeType());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_chmod_when_writing(): void\n    {\n        $this->connection->failOnChmod('/upload/path.txt');\n        $adapter = $this->adapter();\n\n        $this->expectException(UnableToWriteFile::class);\n\n        $adapter->write('path.txt', 'contents', new Config(['visibility' => 'public']));\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_move_a_file_cause_the_parent_directory_cant_be_created(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n\n        $this->expectException(UnableToMoveFile::class);\n\n        $adapter->move('path.txt', 'new-path.txt', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_copy_a_file(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n\n        $this->expectException(UnableToCopyFile::class);\n\n        $adapter->copy('path.txt', 'new-path.txt', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_copy_a_file_because_writing_fails(): void\n    {\n        $this->givenWeHaveAnExistingFile('path.txt', 'contents');\n        $adapter = $this->adapter();\n        $this->connection->failOnPut('/upload/new-path.txt');\n\n        $this->expectException(UnableToCopyFile::class);\n\n        $adapter->copy('path.txt', 'new-path.txt', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_chmod_when_writing_with_a_stream(): void\n    {\n        $writeStream = stream_with_contents('contents');\n        $this->connection->failOnChmod('/upload/path.txt');\n        $adapter = $this->adapter();\n\n        $this->expectException(UnableToWriteFile::class);\n\n        try {\n            $adapter->writeStream('path.txt', $writeStream, new Config(['visibility' => 'public']));\n        } finally {\n            @fclose($writeStream);\n        }\n    }\n\n    /**\n     * @test\n     */\n    public function list_contents_directory_does_not_exist(): void\n    {\n        $contents = $this->adapter()->listContents('/does_not_exist', false);\n        $this->assertCount(0, iterator_to_array($contents));\n    }\n\n    private static function connectionProvider(): ConnectionProvider\n    {\n        if ( ! static::$connectionProvider instanceof ConnectionProvider) {\n            static::$connectionProvider = new StubSftpConnectionProvider('localhost', 'foo', 'pass', 2222);\n        }\n\n        return static::$connectionProvider;\n    }\n\n    /**\n     * @return SftpAdapter\n     */\n    private function adapterWithInvalidRoot(): SftpAdapter\n    {\n        $provider = static::connectionProvider();\n        $adapter = new SftpAdapter($provider, '/invalid');\n\n        return $adapter;\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV2/SftpConnectionProvider.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse phpseclib\\Crypt\\RSA;\nuse phpseclib\\Net\\SFTP;\nuse phpseclib\\System\\SSH\\Agent;\nuse Throwable;\n\n/**\n * @deprecated The \"League\\Flysystem\\PhpseclibV2\\SftpConnectionProvider\" class is deprecated since Flysystem 3.0, use \"League\\Flysystem\\PhpseclibV3\\SftpConnectionProvider\" instead.\n */\nclass SftpConnectionProvider implements ConnectionProvider\n{\n    /**\n     * @var string\n     */\n    private $host;\n\n    /**\n     * @var string\n     */\n    private $username;\n\n    /**\n     * @var string|null\n     */\n    private $password;\n\n    /**\n     * @var bool\n     */\n    private $useAgent;\n\n    /**\n     * @var int\n     */\n    private $port;\n\n    /**\n     * @var int\n     */\n    private $timeout;\n\n    /**\n     * @var SFTP|null\n     */\n    private $connection;\n\n    /**\n     * @var ConnectivityChecker\n     */\n    private $connectivityChecker;\n\n    /**\n     * @var string|null\n     */\n    private $hostFingerprint;\n\n    /**\n     * @var string|null\n     */\n    private $privateKey;\n\n    /**\n     * @var string|null\n     */\n    private $passphrase;\n\n    /**\n     * @var int\n     */\n    private $maxTries;\n\n    public function __construct(\n        string $host,\n        string $username,\n        ?string $password = null,\n        ?string $privateKey = null,\n        ?string $passphrase = null,\n        int $port = 22,\n        bool $useAgent = false,\n        int $timeout = 10,\n        int $maxTries = 4,\n        ?string $hostFingerprint = null,\n        ?ConnectivityChecker $connectivityChecker = null,\n        private bool $disableStatCache = true,\n    ) {\n        $this->host = $host;\n        $this->username = $username;\n        $this->password = $password;\n        $this->privateKey = $privateKey;\n        $this->passphrase = $passphrase;\n        $this->useAgent = $useAgent;\n        $this->port = $port;\n        $this->timeout = $timeout;\n        $this->hostFingerprint = $hostFingerprint;\n        $this->connectivityChecker = $connectivityChecker ?? new SimpleConnectivityChecker();\n        $this->maxTries = $maxTries;\n    }\n\n    public function provideConnection(): SFTP\n    {\n        $tries = 0;\n        start:\n\n        $connection = $this->connection instanceof SFTP\n            ? $this->connection\n            : $this->setupConnection();\n\n        if ( ! $this->connectivityChecker->isConnected($connection)) {\n            $connection->disconnect();\n            $this->connection = null;\n\n            if ($tries < $this->maxTries) {\n                $tries++;\n                goto start;\n            }\n\n            throw UnableToConnectToSftpHost::atHostname($this->host);\n        }\n\n        return $this->connection = $connection;\n    }\n\n    private function setupConnection(): SFTP\n    {\n        $connection = new SFTP($this->host, $this->port, $this->timeout);\n        $this->disableStatCache && $connection->disableStatCache();\n\n        try {\n            $this->checkFingerprint($connection);\n            $this->authenticate($connection);\n        } catch (Throwable $exception) {\n            $connection->disconnect();\n            throw $exception;\n        }\n\n        return $connection;\n    }\n\n    private function checkFingerprint(SFTP $connection): void\n    {\n        if ( ! $this->hostFingerprint) {\n            return;\n        }\n\n        $publicKey = $connection->getServerPublicHostKey();\n\n        if ($publicKey === false) {\n            throw UnableToEstablishAuthenticityOfHost::becauseTheAuthenticityCantBeEstablished($this->host);\n        }\n\n        $fingerprint = $this->getFingerprintFromPublicKey($publicKey);\n\n        if (0 !== strcasecmp($this->hostFingerprint, $fingerprint)) {\n            throw UnableToEstablishAuthenticityOfHost::becauseTheAuthenticityCantBeEstablished($this->host);\n        }\n    }\n\n    private function getFingerprintFromPublicKey(string $publicKey): string\n    {\n        $content = explode(' ', $publicKey, 3);\n\n        return implode(':', str_split(md5(base64_decode($content[1])), 2));\n    }\n\n    private function authenticate(SFTP $connection): void\n    {\n        if ($this->privateKey !== null) {\n            $this->authenticateWithPrivateKey($connection);\n        } elseif ($this->useAgent) {\n            $this->authenticateWithAgent($connection);\n        } elseif ( ! $connection->login($this->username, $this->password)) {\n            throw UnableToAuthenticate::withPassword();\n        }\n    }\n\n    public static function fromArray(array $options): SftpConnectionProvider\n    {\n        return new SftpConnectionProvider(\n            $options['host'],\n            $options['username'],\n            $options['password'] ?? null,\n            $options['privateKey'] ?? null,\n            $options['passphrase'] ?? null,\n            $options['port'] ?? 22,\n            $options['useAgent'] ?? false,\n            $options['timeout'] ?? 10,\n            $options['maxTries'] ?? 4,\n            $options['hostFingerprint'] ?? null,\n            $options['connectivityChecker'] ?? null\n        );\n    }\n\n    private function authenticateWithPrivateKey(SFTP $connection): void\n    {\n        $privateKey = $this->loadPrivateKey();\n\n        if ($connection->login($this->username, $privateKey)) {\n            return;\n        }\n\n        if ($this->password !== null && $connection->login($this->username, $this->password)) {\n            return;\n        }\n\n        throw UnableToAuthenticate::withPrivateKey();\n    }\n\n    private function loadPrivateKey(): RSA\n    {\n        if (\"---\" !== substr($this->privateKey, 0, 3) && is_file($this->privateKey)) {\n            $this->privateKey = file_get_contents($this->privateKey);\n        }\n\n        $key = new RSA();\n\n        if ($this->passphrase !== null) {\n            $key->setPassword($this->passphrase);\n        }\n\n        if ( ! $key->loadKey($this->privateKey)) {\n            throw new UnableToLoadPrivateKey();\n        }\n\n        return $key;\n    }\n\n    private function authenticateWithAgent(SFTP $connection): void\n    {\n        $agent = new Agent();\n\n        if ( ! $connection->login($this->username, $agent)) {\n            throw UnableToAuthenticate::withSshAgent();\n        }\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV2/SftpConnectionProviderTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse phpseclib\\Net\\SFTP;\nuse PHPUnit\\Framework\\TestCase;\n\nuse function class_exists;\n\n/**\n * @group sftp\n * @group sftp-connection\n * @group legacy\n */\nclass SftpConnectionProviderTest extends TestCase\n{\n    protected function setUp(): void\n    {\n        if ( ! class_exists('phpseclib\\Net\\SFTP')) {\n            self::markTestSkipped(\"PHPSecLib V2 is not installed\");\n        }\n\n        parent::setUp();\n    }\n\n    /**\n     * @test\n     */\n    public function giving_up_after_5_connection_failures(): void\n    {\n        $this->expectException(UnableToConnectToSftpHost::class);\n\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'password' => 'pass',\n                'port' => 2222,\n                'timeout' => 10,\n                'connectivityChecker' => new FixatedConnectivityChecker(5)\n            ]\n        );\n\n        $provider->provideConnection();\n    }\n\n    /**\n     * @test\n     */\n    public function trying_until_5_tries(): void\n    {\n        $provider = SftpConnectionProvider::fromArray([\n            'host' => 'localhost',\n            'username' => 'foo',\n            'password' => 'pass',\n            'port' => 2222,\n            'timeout' => 10,\n            'connectivityChecker' => new FixatedConnectivityChecker(4)\n        ]);\n        $connection = $provider->provideConnection();\n        $sameConnection = $provider->provideConnection();\n\n        $this->assertInstanceOf(SFTP::class, $connection);\n        $this->assertSame($connection, $sameConnection);\n    }\n\n    /**\n     * @test\n     */\n    public function authenticating_with_a_private_key(): void\n    {\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'bar',\n                'privateKey' => __DIR__ . '/../../test_files/sftp/id_rsa',\n                'passphrase' => 'secret',\n                'port' => 2222,\n            ]\n        );\n\n        $connection = $provider->provideConnection();\n        $this->assertInstanceOf(SFTP::class, $connection);\n    }\n\n    /**\n     * @test\n     */\n    public function authenticating_with_an_invalid_private_key(): void\n    {\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'bar',\n                'privateKey' => __DIR__ . '/../../test_files/sftp/users.conf',\n                'port' => 2222,\n            ]\n        );\n\n        $this->expectException(UnableToLoadPrivateKey::class);\n\n        $provider->provideConnection();\n    }\n\n    /**\n     * @test\n     */\n    public function authenticating_with_an_ssh_agent(): void\n    {\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'bar',\n                'useAgent' => true,\n                'port' => 2222,\n            ]\n        );\n\n        $connection = $provider->provideConnection();\n        $this->assertInstanceOf(SFTP::class, $connection);\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_authenticating_with_an_ssh_agent(): void\n    {\n        $this->expectException(UnableToAuthenticate::class);\n\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'useAgent' => true,\n                'port' => 2222,\n            ]\n        );\n\n        $provider->provideConnection();\n    }\n\n    /**\n     * @test\n     */\n    public function authenticating_with_a_private_key_and_falling_back_to_password(): void\n    {\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'password' => 'pass',\n                'privateKey' => __DIR__ . '/../../test_files/sftp/id_rsa',\n                'passphrase' => 'secret',\n                'port' => 2222,\n            ]\n        );\n\n        $connection = $provider->provideConnection();\n        $this->assertInstanceOf(SFTP::class, $connection);\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_authenticate_with_a_private_key(): void\n    {\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'privateKey' => __DIR__ . '/../../test_files/sftp/unknown.key',\n                'passphrase' => 'secret',\n                'port' => 2222,\n            ]\n        );\n\n        $this->expectExceptionObject(UnableToAuthenticate::withPrivateKey());\n        $provider->provideConnection();\n    }\n\n    /**\n     * @test\n     */\n    public function verifying_a_fingerprint(): void\n    {\n        $key = file_get_contents(__DIR__ . '/../../test_files/sftp/ssh_host_rsa_key.pub');\n        $fingerPrint = $this->computeFingerPrint($key);\n\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'password' => 'pass',\n                'port' => 2222,\n                'hostFingerprint' => $fingerPrint,\n            ]\n        );\n\n        $anotherConnection = $provider->provideConnection();\n        $this->assertInstanceOf(SFTP::class, $anotherConnection);\n    }\n\n    /**\n     * @test\n     */\n    public function providing_an_invalid_fingerprint(): void\n    {\n        $this->expectException(UnableToEstablishAuthenticityOfHost::class);\n\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'password' => 'pass',\n                'port' => 2222,\n                'hostFingerprint' => 'invalid:fingerprint',\n            ]\n        );\n        $provider->provideConnection();\n    }\n\n    /**\n     * @test\n     */\n    public function providing_an_invalid_password(): void\n    {\n        $this->expectException(UnableToAuthenticate::class);\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'password' => 'lol',\n                'port' => 2222,\n            ]\n        );\n        $provider->provideConnection();\n    }\n\n    private function computeFingerPrint(string $publicKey): string\n    {\n        $content = explode(' ', $publicKey, 3);\n\n        return implode(':', str_split(md5(base64_decode($content[1])), 2));\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV2/SftpStub.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse phpseclib\\Net\\SFTP;\n\n/**\n * @internal This is only used for testing purposes.\n *\n * @deprecated The \"League\\Flysystem\\PhpseclibV2\\SftpStub\" class is deprecated since Flysystem 3.0, use \"League\\Flysystem\\PhpseclibV3\\SftpStub\" instead.\n */\nclass SftpStub extends SFTP\n{\n    /**\n     * @var array<string,bool>\n     */\n    private $tripWires = [];\n\n    public function failOnChmod(string $filename): void\n    {\n        $key = $this->formatTripKey('chmod', $filename);\n        $this->tripWires[$key] = true;\n    }\n\n    /**\n     * @param int    $mode\n     * @param string $filename\n     * @param bool   $recursive\n     *\n     * @return bool|mixed\n     */\n    public function chmod($mode, $filename, $recursive = false)\n    {\n        $key = $this->formatTripKey('chmod', $filename);\n        $shouldTrip = $this->tripWires[$key] ?? false;\n\n        if ($shouldTrip) {\n            unset($this->tripWires[$key]);\n\n            return false;\n        }\n\n        return parent::chmod($mode, $filename, $recursive);\n    }\n\n    public function failOnPut(string $filename): void\n    {\n        $key = $this->formatTripKey('put', $filename);\n        $this->tripWires[$key] = true;\n    }\n\n    /**\n     * @param string          $remote_file\n     * @param resource|string $data\n     * @param int             $mode\n     * @param int             $start\n     * @param int             $local_start\n     * @param null            $progressCallback\n     *\n     * @return bool\n     */\n    public function put(\n        $remote_file,\n        $data,\n        $mode = self::SOURCE_STRING,\n        $start = -1,\n        $local_start = -1,\n        $progressCallback = null\n    ) {\n        $key = $this->formatTripKey('put', $remote_file);\n        $shouldTrip = $this->tripWires[$key] ?? false;\n\n        if ($shouldTrip) {\n            return false;\n        }\n\n        return parent::put($remote_file, $data, $mode, $start, $local_start, $progressCallback);\n    }\n\n    /**\n     * @param array<int,mixed> $arguments\n     *\n     * @return string\n     */\n    private function formatTripKey(...$arguments): string\n    {\n        $key = '';\n\n        foreach ($arguments as $argument) {\n            $key .= var_export($argument, true);\n        }\n\n        return $key;\n    }\n\n    public function reset(): void\n    {\n        $this->tripWires = [];\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV2/SimpleConnectivityChecker.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse phpseclib\\Net\\SFTP;\n\n/**\n * @deprecated The \"League\\Flysystem\\PhpseclibV2\\SimpleConnectivityChecker\" class is deprecated since Flysystem 3.0, use \"League\\Flysystem\\PhpseclibV3\\SimpleConnectivityChecker\" instead.\n */\nclass SimpleConnectivityChecker implements ConnectivityChecker\n{\n    public function isConnected(SFTP $connection): bool\n    {\n        return $connection->isConnected();\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV2/StubSftpConnectionProvider.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse phpseclib\\Net\\SFTP;\n\n/**\n * @deprecated The \"League\\Flysystem\\PhpseclibV2\\StubSftpConnectionProvider\" class is deprecated since Flysystem 3.0, use \"League\\Flysystem\\PhpseclibV3\\StubSftpConnectionProvider\" instead.\n */\nclass StubSftpConnectionProvider implements ConnectionProvider\n{\n    /**\n     * @var string\n     */\n    private $host;\n\n    /**\n     * @var string\n     */\n    private $username;\n\n    /**\n     * @var string|null\n     */\n    private $password;\n\n    /**\n     * @var int\n     */\n    private $port;\n\n    /**\n     * @var SftpStub\n     */\n    private $connection;\n\n    public function __construct(\n        string $host,\n        string $username,\n        ?string $password = null,\n        int $port = 22\n    ) {\n        $this->host = $host;\n        $this->username = $username;\n        $this->password = $password;\n        $this->port = $port;\n    }\n\n    public function provideConnection(): SFTP\n    {\n        if ( ! $this->connection instanceof SFTP) {\n            $connection = new SftpStub($this->host, $this->port);\n            $connection->login($this->username, $this->password);\n\n            $this->connection = $connection;\n        }\n\n        return $this->connection;\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV2/UnableToAuthenticate.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse League\\Flysystem\\FilesystemException;\nuse RuntimeException;\n\n/**\n * @deprecated The \"League\\Flysystem\\PhpseclibV2\\UnableToAuthenticate\" class is deprecated since Flysystem 3.0, use \"League\\Flysystem\\PhpseclibV3\\UnableToAuthenticate\" instead.\n */\nclass UnableToAuthenticate extends RuntimeException implements FilesystemException\n{\n    public static function withPassword(): UnableToAuthenticate\n    {\n        return new UnableToAuthenticate('Unable to authenticate using a password.');\n    }\n\n    public static function withPrivateKey(): UnableToAuthenticate\n    {\n        return new UnableToAuthenticate('Unable to authenticate using a private key.');\n    }\n\n    public static function withSshAgent(): UnableToAuthenticate\n    {\n        return new UnableToAuthenticate('Unable to authenticate using an SSH agent.');\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV2/UnableToConnectToSftpHost.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse League\\Flysystem\\FilesystemException;\nuse RuntimeException;\n\n/**\n * @deprecated The \"League\\Flysystem\\PhpseclibV2\\UnableToConnectToSftpHost\" class is deprecated since Flysystem 3.0, use \"League\\Flysystem\\PhpseclibV3\\UnableToConnectToSftpHost\" instead.\n */\nclass UnableToConnectToSftpHost extends RuntimeException implements FilesystemException\n{\n    public static function atHostname(string $host): UnableToConnectToSftpHost\n    {\n        return new UnableToConnectToSftpHost(\"Unable to connect to host: $host\");\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV2/UnableToEstablishAuthenticityOfHost.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse League\\Flysystem\\FilesystemException;\nuse RuntimeException;\n\n/**\n * @deprecated The \"League\\Flysystem\\PhpseclibV2\\UnableToEstablishAuthenticityOfHost\" class is deprecated since Flysystem 3.0, use \"League\\Flysystem\\PhpseclibV3\\UnableToEstablishAuthenticityOfHost\" instead.\n */\nclass UnableToEstablishAuthenticityOfHost extends RuntimeException implements FilesystemException\n{\n    public static function becauseTheAuthenticityCantBeEstablished(string $host): UnableToEstablishAuthenticityOfHost\n    {\n        return new UnableToEstablishAuthenticityOfHost(\"The authenticity of host $host can't be established.\");\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV2/UnableToLoadPrivateKey.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV2;\n\nuse League\\Flysystem\\FilesystemException;\nuse RuntimeException;\n\n/**\n * @deprecated The \"League\\Flysystem\\PhpseclibV2\\UnableToLoadPrivateKey\" class is deprecated since Flysystem 3.0, use \"League\\Flysystem\\PhpseclibV3\\UnableToLoadPrivateKey\" instead.\n */\nclass UnableToLoadPrivateKey extends RuntimeException implements FilesystemException\n{\n    public function __construct(string $message = \"Unable to load private key.\")\n    {\n        parent::__construct($message);\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV2/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-sftp\",\n    \"description\": \"SFTP filesystem adapter for Flysystem.\",\n    \"keywords\": [\"flysystem\", \"filesystem\", \"sftp\", \"files\", \"file\"],\n    \"autoload\": {\n        \"psr-4\": {\n                \"League\\\\Flysystem\\\\PhpseclibV2\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"league/flysystem\": \"^3.0.0\",\n        \"league/mime-type-detection\": \"^1.0.0\",\n        \"phpseclib/phpseclib\": \"^2.0\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ],\n    \"abandoned\": \"league/flysystem-sftp-v3\"\n}\n"
  },
  {
    "path": "src/PhpseclibV3/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n**/*Test.php export-ignore\n**/*Stub.php export-ignore\nREADME.md export-ignore"
  },
  {
    "path": "src/PhpseclibV3/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/PhpseclibV3/ConnectionProvider.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse phpseclib3\\Net\\SFTP;\n\n/**\n * @method void disconnect()\n */\ninterface ConnectionProvider\n{\n    public function provideConnection(): SFTP;\n}\n"
  },
  {
    "path": "src/PhpseclibV3/ConnectivityChecker.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse phpseclib3\\Net\\SFTP;\n\ninterface ConnectivityChecker\n{\n    public function isConnected(SFTP $connection): bool;\n}\n"
  },
  {
    "path": "src/PhpseclibV3/FixatedConnectivityChecker.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse phpseclib3\\Net\\SFTP;\n\nclass FixatedConnectivityChecker implements ConnectivityChecker\n{\n    /**\n     * @var int\n     */\n    private $succeedAfter;\n\n    /**\n     * @var int\n     */\n    private $numberOfTimesChecked = 0;\n\n    public function __construct(int $succeedAfter = 0)\n    {\n        $this->succeedAfter = $succeedAfter;\n    }\n\n    public function isConnected(SFTP $connection): bool\n    {\n        if ($this->numberOfTimesChecked >= $this->succeedAfter) {\n            return true;\n        }\n\n        $this->numberOfTimesChecked++;\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV3/LICENSE",
    "content": "Copyright (c) 2013-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/PhpseclibV3/README.md",
    "content": "## Sub-split of Flysystem for SFTP using phpseclib v3.\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n\n```bash\ncomposer require league/flysystem-sftp-v3\n```\n\nView the [documentation](https://flysystem.thephpleague.com/docs/adapter/sftp-v3/).\n"
  },
  {
    "path": "src/PhpseclibV3/SftpAdapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\FilesystemException;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\StorageAttributes;\nuse League\\Flysystem\\UnableToCheckDirectoryExistence;\nuse League\\Flysystem\\UnableToCheckFileExistence;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UnixVisibility\\PortableVisibilityConverter;\nuse League\\Flysystem\\UnixVisibility\\VisibilityConverter;\nuse League\\MimeTypeDetection\\FinfoMimeTypeDetector;\nuse League\\MimeTypeDetection\\MimeTypeDetector;\nuse phpseclib3\\Net\\SFTP;\nuse Throwable;\n\nuse function rtrim;\n\nclass SftpAdapter implements FilesystemAdapter\n{\n    private VisibilityConverter $visibilityConverter;\n    private PathPrefixer $prefixer;\n    private MimeTypeDetector $mimeTypeDetector;\n\n    public function __construct(\n        private ConnectionProvider $connectionProvider,\n        string $root,\n        ?VisibilityConverter $visibilityConverter = null,\n        ?MimeTypeDetector $mimeTypeDetector = null,\n        private bool $detectMimeTypeUsingPath = false,\n        private bool $disconnectOnDestruct = false,\n    ) {\n        $this->prefixer = new PathPrefixer($root);\n        $this->visibilityConverter = $visibilityConverter ?? new PortableVisibilityConverter();\n        $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();\n    }\n\n    public function fileExists(string $path): bool\n    {\n        $location = $this->prefixer->prefixPath($path);\n\n        try {\n            return $this->connectionProvider->provideConnection()->is_file($location);\n        } catch (Throwable $exception) {\n            throw UnableToCheckFileExistence::forLocation($path, $exception);\n        }\n    }\n\n    public function disconnect(): void\n    {\n        $this->connectionProvider->disconnect();\n    }\n\n    public function directoryExists(string $path): bool\n    {\n        $location = $this->prefixer->prefixDirectoryPath($path);\n\n        try {\n            return $this->connectionProvider->provideConnection()->is_dir($location);\n        } catch (Throwable $exception) {\n            throw UnableToCheckDirectoryExistence::forLocation($path, $exception);\n        }\n    }\n\n    /**\n     * @param string          $path\n     * @param string|resource $contents\n     * @param Config          $config\n     *\n     * @throws FilesystemException\n     */\n    private function upload(string $path, $contents, Config $config): void\n    {\n        $this->ensureParentDirectoryExists($path, $config);\n        $connection = $this->connectionProvider->provideConnection();\n        $location = $this->prefixer->prefixPath($path);\n\n        if ( ! $connection->put($location, $contents, SFTP::SOURCE_STRING)) {\n            throw UnableToWriteFile::atLocation($path, 'not able to write the file');\n        }\n\n        if ($visibility = $config->get(Config::OPTION_VISIBILITY)) {\n            $this->setVisibility($path, $visibility);\n        }\n    }\n\n    private function ensureParentDirectoryExists(string $path, Config $config): void\n    {\n        $parentDirectory = dirname($path);\n\n        if ($parentDirectory === '' || $parentDirectory === '.') {\n            return;\n        }\n\n        /** @var string $visibility */\n        $visibility = $config->get(Config::OPTION_DIRECTORY_VISIBILITY);\n        $this->makeDirectory($parentDirectory, $visibility);\n    }\n\n    private function makeDirectory(string $directory, ?string $visibility): void\n    {\n        $location = $this->prefixer->prefixPath($directory);\n        $connection = $this->connectionProvider->provideConnection();\n\n        if ($connection->is_dir($location)) {\n            return;\n        }\n\n        $mode = $visibility ? $this->visibilityConverter->forDirectory(\n            $visibility\n        ) : $this->visibilityConverter->defaultForDirectories();\n\n        if ( ! $connection->mkdir($location, $mode, true) && ! $connection->is_dir($location)) {\n            throw UnableToCreateDirectory::atLocation($directory);\n        }\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        try {\n            $this->upload($path, $contents, $config);\n        } catch (UnableToWriteFile $exception) {\n            throw $exception;\n        } catch (Throwable $exception) {\n            throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        try {\n            $this->upload($path, $contents, $config);\n        } catch (UnableToWriteFile $exception) {\n            throw $exception;\n        } catch (Throwable $exception) {\n            throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function read(string $path): string\n    {\n        $location = $this->prefixer->prefixPath($path);\n        $connection = $this->connectionProvider->provideConnection();\n        $contents = $connection->get($location);\n\n        if ( ! is_string($contents)) {\n            throw UnableToReadFile::fromLocation($path);\n        }\n\n        return $contents;\n    }\n\n    public function readStream(string $path)\n    {\n        $location = $this->prefixer->prefixPath($path);\n        $connection = $this->connectionProvider->provideConnection();\n        /** @var resource $readStream */\n        $readStream = fopen('php://temp', 'w+');\n\n        if ( ! $connection->get($location, $readStream)) {\n            fclose($readStream);\n            throw UnableToReadFile::fromLocation($path);\n        }\n\n        rewind($readStream);\n\n        return $readStream;\n    }\n\n    public function delete(string $path): void\n    {\n        $location = $this->prefixer->prefixPath($path);\n        $connection = $this->connectionProvider->provideConnection();\n        $connection->delete($location);\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        $location = rtrim($this->prefixer->prefixPath($path), '/') . '/';\n        $connection = $this->connectionProvider->provideConnection();\n        $connection->delete($location);\n        $connection->rmdir($location);\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        $this->makeDirectory($path, $config->get(Config::OPTION_DIRECTORY_VISIBILITY, $config->get(Config::OPTION_VISIBILITY)));\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        $location = $this->prefixer->prefixPath($path);\n        $connection = $this->connectionProvider->provideConnection();\n        $mode = $this->visibilityConverter->forFile($visibility);\n\n        if ( ! $connection->chmod($mode, $location, false)) {\n            throw UnableToSetVisibility::atLocation($path);\n        }\n    }\n\n    private function fetchFileMetadata(string $path, string $type): FileAttributes\n    {\n        $location = $this->prefixer->prefixPath($path);\n        $connection = $this->connectionProvider->provideConnection();\n        $stat = $connection->stat($location);\n\n        if ( ! is_array($stat)) {\n            throw UnableToRetrieveMetadata::create($path, $type);\n        }\n\n        $attributes = $this->convertListingToAttributes($path, $stat);\n\n        if ( ! $attributes instanceof FileAttributes) {\n            throw UnableToRetrieveMetadata::create($path, $type, 'path is not a file');\n        }\n\n        return $attributes;\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        try {\n            $mimetype = $this->detectMimeTypeUsingPath\n                ? $this->mimeTypeDetector->detectMimeTypeFromPath($path)\n                : $this->mimeTypeDetector->detectMimeType($path, $this->read($path));\n        } catch (Throwable $exception) {\n            throw UnableToRetrieveMetadata::mimeType($path, $exception->getMessage(), $exception);\n        }\n\n        if ($mimetype === null) {\n            throw UnableToRetrieveMetadata::mimeType($path, 'Unknown.');\n        }\n\n        return new FileAttributes($path, null, null, null, $mimetype);\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_LAST_MODIFIED);\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_FILE_SIZE);\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_VISIBILITY);\n    }\n\n    public function listContents(string $path, bool $deep): iterable\n    {\n        $connection = $this->connectionProvider->provideConnection();\n        $location = $this->prefixer->prefixPath(rtrim($path, '/')) . '/';\n        $listing = $connection->rawlist($location, false);\n\n        if (false === $listing) {\n            return;\n        }\n\n        foreach ($listing as $filename => $attributes) {\n            if ($filename === '.' || $filename === '..') {\n                continue;\n            }\n\n            // Ensure numeric keys are strings.\n            $filename = (string) $filename;\n            $path = $this->prefixer->stripPrefix($location . ltrim($filename, '/'));\n            $attributes = $this->convertListingToAttributes($path, $attributes);\n            yield $attributes;\n\n            if ($deep && $attributes->isDir()) {\n                foreach ($this->listContents($attributes->path(), true) as $child) {\n                    yield $child;\n                }\n            }\n        }\n    }\n\n    private function convertListingToAttributes(string $path, array $attributes): StorageAttributes\n    {\n        $permissions = $attributes['mode'] & 0777;\n        $lastModified = $attributes['mtime'] ?? null;\n\n        if (($attributes['type'] ?? null) === NET_SFTP_TYPE_DIRECTORY) {\n            return new DirectoryAttributes(\n                ltrim($path, '/'),\n                $this->visibilityConverter->inverseForDirectory($permissions),\n                $lastModified\n            );\n        }\n\n        return new FileAttributes(\n            $path,\n            $attributes['size'],\n            $this->visibilityConverter->inverseForFile($permissions),\n            $lastModified\n        );\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        $sourceLocation = $this->prefixer->prefixPath($source);\n        $destinationLocation = $this->prefixer->prefixPath($destination);\n        $connection = $this->connectionProvider->provideConnection();\n\n        try {\n            $this->ensureParentDirectoryExists($destination, $config);\n        } catch (Throwable $exception) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);\n        }\n\n        if ($sourceLocation === $destinationLocation) {\n            return;\n        }\n\n        if ($connection->rename($sourceLocation, $destinationLocation)) {\n            return;\n        }\n\n        // Overwrite existing file / dir\n        if ($connection->is_file($destinationLocation)) {\n            $this->delete($destination);\n            if ($connection->rename($sourceLocation, $destinationLocation)) {\n                return;\n            }\n        }\n\n        throw UnableToMoveFile::fromLocationTo($source, $destination);\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        try {\n            $readStream = $this->readStream($source);\n            $visibility = $config->get(Config::OPTION_VISIBILITY);\n\n            if ($visibility === null && $config->get(Config::OPTION_RETAIN_VISIBILITY, true)) {\n                $config = $config->withSetting(Config::OPTION_VISIBILITY, $this->visibility($source)->visibility());\n            }\n\n            $this->writeStream($destination, $readStream, $config);\n        } catch (Throwable $exception) {\n            if (isset($readStream) && is_resource($readStream)) {\n                @fclose($readStream);\n            }\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    public function __destruct()\n    {\n        if ($this->disconnectOnDestruct) {\n            $this->connectionProvider->disconnect();\n        }\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV3/SftpAdapterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse League\\Flysystem\\AdapterTestUtilities\\FilesystemAdapterTestCase;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\Visibility;\nuse phpseclib3\\Net\\SFTP;\n\nuse function class_exists;\n\n/**\n * @group sftp\n * @group phpseclib3\n */\nclass SftpAdapterTest extends FilesystemAdapterTestCase\n{\n    public static function setUpBeforeClass(): void\n    {\n        if ( ! class_exists(SFTP::class)) {\n            self::markTestIncomplete(\"No phpseclib v3 installed\");\n        }\n    }\n\n    /**\n     * @var StubSftpConnectionProvider\n     */\n    private static $connectionProvider;\n\n    /**\n     * @var SftpStub\n     */\n    private $connection;\n\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        return new SftpAdapter(\n            static::connectionProvider(),\n            '/upload'\n        );\n    }\n\n    /**\n     * @before\n     */\n    public function setupConnectionProvider(): void\n    {\n        /** @var SftpStub $connection */\n        $connection = static::connectionProvider()->provideConnection();\n        $this->connection = $connection;\n        $this->connection->resetTripWires();\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_create_a_directory(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n\n        $this->expectException(UnableToCreateDirectory::class);\n\n        $adapter->createDirectory('not-gonna-happen', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_write_a_file(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n\n        $this->expectException(UnableToWriteFile::class);\n\n        $adapter->write('not-gonna-happen', 'na-ah', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_read_a_file(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n\n        $this->expectException(UnableToReadFile::class);\n\n        $adapter->read('not-gonna-happen');\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_read_a_file_as_a_stream(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n\n        $this->expectException(UnableToReadFile::class);\n\n        $adapter->readStream('not-gonna-happen');\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_write_a_file_using_streams(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n        $writeHandle = stream_with_contents('contents');\n\n        $this->expectException(UnableToWriteFile::class);\n\n        try {\n            $adapter->writeStream('not-gonna-happen', $writeHandle, new Config());\n        } finally {\n            fclose($writeHandle);\n        }\n    }\n\n    /**\n     * @test\n     */\n    public function detecting_mimetype(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->write('file.svg', (string) file_get_contents(__DIR__ . '/../AdapterTestUtilities/test_files/flysystem.svg'), new Config());\n\n        $mimeType = $adapter->mimeType('file.svg');\n\n        $this->assertStringStartsWith('image/svg+xml', $mimeType->mimeType());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_chmod_when_writing(): void\n    {\n        $this->connection->failOnChmod('/upload/path.txt');\n        $adapter = $this->adapter();\n\n        $this->expectException(UnableToWriteFile::class);\n\n        $adapter->write('path.txt', 'contents', new Config(['visibility' => 'public']));\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_move_a_file_cause_the_parent_directory_cant_be_created(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n\n        $this->expectException(UnableToMoveFile::class);\n\n        $adapter->move('path.txt', 'new-path.txt', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_copy_a_file(): void\n    {\n        $adapter = $this->adapterWithInvalidRoot();\n\n        $this->expectException(UnableToCopyFile::class);\n\n        $adapter->copy('path.txt', 'new-path.txt', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_copy_a_file_because_writing_fails(): void\n    {\n        $this->givenWeHaveAnExistingFile('path.txt', 'contents');\n        $adapter = $this->adapter();\n        $this->connection->failOnPut('/upload/new-path.txt');\n\n        $this->expectException(UnableToCopyFile::class);\n\n        $adapter->copy('path.txt', 'new-path.txt', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_chmod_when_writing_with_a_stream(): void\n    {\n        $writeStream = stream_with_contents('contents');\n        $this->connection->failOnChmod('/upload/path.txt');\n        $adapter = $this->adapter();\n\n        $this->expectException(UnableToWriteFile::class);\n\n        try {\n            $adapter->writeStream('path.txt', $writeStream, new Config(['visibility' => 'public']));\n        } finally {\n            @fclose($writeStream);\n        }\n    }\n\n    /**\n     * @test\n     */\n    public function list_contents_directory_does_not_exist(): void\n    {\n        $contents = $this->adapter()->listContents('/does_not_exist', false);\n        $this->assertCount(0, iterator_to_array($contents));\n    }\n\n    /**\n     * @test\n     */\n    public function it_can_proactively_close_a_connection(): void\n    {\n        /** @var SftpAdapter $adapter */\n        $adapter = $this->adapter();\n\n        self::assertFalse($adapter->fileExists('does not exists at all'));\n\n        self::assertTrue(static::$connectionProvider->connection->isConnected());\n\n        $adapter->disconnect();\n\n        self::assertFalse(static::$connectionProvider->connection->isConnected());\n    }\n    /**\n     * @test\n     * @fixme Move to FilesystemAdapterTestCase once all adapters pass\n     */\n    public function moving_a_file_and_overwriting(): void\n    {\n        $this->runScenario(function() {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be moved',\n                new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])\n            );\n            $adapter->write(\n                'destination.txt',\n                'contents to be overwritten',\n                new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])\n            );\n            $adapter->move('source.txt', 'destination.txt', new Config());\n            $this->assertFalse(\n                $adapter->fileExists('source.txt'),\n                'After moving a file should no longer exist in the original location.'\n            );\n            $this->assertTrue(\n                $adapter->fileExists('destination.txt'),\n                'After moving, a file should be present at the new location.'\n            );\n            $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('destination.txt')->visibility());\n            $this->assertEquals('contents to be moved', $adapter->read('destination.txt'));\n        });\n    }\n\n    private static function connectionProvider(): StubSftpConnectionProvider\n    {\n        if ( ! static::$connectionProvider instanceof ConnectionProvider) {\n            static::$connectionProvider = new StubSftpConnectionProvider('localhost', 'foo', 'pass', 2222);\n        }\n\n        return static::$connectionProvider;\n    }\n\n    /**\n     * @return SftpAdapter\n     */\n    private function adapterWithInvalidRoot(): SftpAdapter\n    {\n        $provider = static::connectionProvider();\n\n        return new SftpAdapter($provider, '/invalid');\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV3/SftpConnectionProvider.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse League\\Flysystem\\FilesystemException;\nuse phpseclib3\\Crypt\\Common\\AsymmetricKey;\nuse phpseclib3\\Crypt\\PublicKeyLoader;\nuse phpseclib3\\Exception\\NoKeyLoadedException;\nuse phpseclib3\\Net\\SFTP;\nuse phpseclib3\\System\\SSH\\Agent;\nuse Throwable;\n\nuse function base64_decode;\nuse function implode;\nuse function str_split;\n\nclass SftpConnectionProvider implements ConnectionProvider\n{\n\n    /**\n     * @var SFTP|null\n     */\n    private $connection;\n\n    /**\n     * @var ConnectivityChecker\n     */\n    private $connectivityChecker;\n\n    public function __construct(\n        private string $host,\n        private string $username,\n        private ?string $password = null,\n        private ?string $privateKey = null,\n        private ?string $passphrase = null,\n        private int $port = 22,\n        private bool $useAgent = false,\n        private int $timeout = 10,\n        private int $maxTries = 4,\n        private ?string $hostFingerprint = null,\n        ?ConnectivityChecker $connectivityChecker = null,\n        private array $preferredAlgorithms = [],\n        private bool $disableStatCache = true,\n    ) {\n        $this->connectivityChecker = $connectivityChecker ?? new SimpleConnectivityChecker();\n    }\n\n    public function provideConnection(): SFTP\n    {\n        $tries = 0;\n        start:\n        $tries++;\n\n        try {\n            $connection = $this->connection instanceof SFTP\n                ? $this->connection\n                : $this->setupConnection();\n        } catch (Throwable $exception) {\n            if ($tries <= $this->maxTries) {\n                goto start;\n            }\n\n            if ($exception instanceof FilesystemException) {\n                throw $exception;\n            }\n\n            throw UnableToConnectToSftpHost::atHostname($this->host, $exception);\n        }\n\n        if ( ! $this->connectivityChecker->isConnected($connection)) {\n            $connection->disconnect();\n            $this->connection = null;\n\n            if ($tries <= $this->maxTries) {\n                goto start;\n            }\n\n            throw UnableToConnectToSftpHost::atHostname($this->host);\n        }\n\n        return $this->connection = $connection;\n    }\n\n    public function disconnect(): void\n    {\n        if ($this->connection) {\n            $this->connection->disconnect();\n            $this->connection = null;\n        }\n    }\n\n    private function setupConnection(): SFTP\n    {\n        $connection = new SFTP($this->host, $this->port, $this->timeout);\n        $connection->setPreferredAlgorithms($this->preferredAlgorithms);\n        $this->disableStatCache && $connection->disableStatCache();\n\n        try {\n            $this->checkFingerprint($connection);\n            $this->authenticate($connection);\n        } catch (Throwable $exception) {\n            $connection->disconnect();\n            throw $exception;\n        }\n\n        return $connection;\n    }\n\n    private function checkFingerprint(SFTP $connection): void\n    {\n        if ( ! $this->hostFingerprint) {\n            return;\n        }\n\n        $publicKey = $connection->getServerPublicHostKey();\n\n        if ($publicKey === false) {\n            throw UnableToEstablishAuthenticityOfHost::becauseTheAuthenticityCantBeEstablished($this->host);\n        }\n\n        $fingerprint = $this->getFingerprintFromPublicKey($publicKey);\n\n        if (0 !== strcasecmp($this->hostFingerprint, $fingerprint)) {\n            throw UnableToEstablishAuthenticityOfHost::becauseTheAuthenticityCantBeEstablished($this->host);\n        }\n    }\n\n    private function getFingerprintFromPublicKey(string $publicKey): string\n    {\n        $content = explode(' ', $publicKey, 3);\n        $algo = $content[0] === 'ssh-rsa' ? 'md5' : 'sha512';\n\n        return implode(':', str_split(hash($algo, base64_decode($content[1])), 2));\n    }\n\n    private function authenticate(SFTP $connection): void\n    {\n        if ($this->privateKey !== null) {\n            $this->authenticateWithPrivateKey($connection);\n        } elseif ($this->useAgent) {\n            $this->authenticateWithAgent($connection);\n        } else {\n            $this->authenticateWithUsernameAndPassword($connection);\n        }\n    }\n\n    private function authenticateWithUsernameAndPassword(SFTP $connection): void\n    {\n        if ( ! $connection->login($this->username, $this->password)) {\n            throw UnableToAuthenticate::withPassword($connection->getLastError());\n        }\n    }\n\n    public static function fromArray(array $options): SftpConnectionProvider\n    {\n        return new SftpConnectionProvider(\n            $options['host'],\n            $options['username'],\n            $options['password'] ?? null,\n            $options['privateKey'] ?? null,\n            $options['passphrase'] ?? null,\n            $options['port'] ?? 22,\n            $options['useAgent'] ?? false,\n            $options['timeout'] ?? 10,\n            $options['maxTries'] ?? 4,\n            $options['hostFingerprint'] ?? null,\n            $options['connectivityChecker'] ?? null,\n            $options['preferredAlgorithms'] ?? [],\n        );\n    }\n\n    private function authenticateWithPrivateKey(SFTP $connection): void\n    {\n        $privateKey = $this->loadPrivateKey();\n\n        if ($connection->login($this->username, $privateKey)) {\n            return;\n        }\n\n        if ($this->password !== null && $connection->login($this->username, $this->password)) {\n            return;\n        }\n\n        throw UnableToAuthenticate::withPrivateKey($connection->getLastError());\n    }\n\n    private function loadPrivateKey(): AsymmetricKey\n    {\n        if ((\"---\" !== substr($this->privateKey, 0, 3) || \"PuTTY\" !== substr($this->privateKey, 0, 5)) && is_file($this->privateKey)) {\n            $this->privateKey = file_get_contents($this->privateKey);\n        }\n\n        try {\n            if ($this->passphrase !== null) {\n                return PublicKeyLoader::load($this->privateKey, $this->passphrase);\n            }\n\n            return PublicKeyLoader::load($this->privateKey);\n        } catch (NoKeyLoadedException $exception) {\n            throw new UnableToLoadPrivateKey(null, $exception);\n        }\n    }\n\n    private function authenticateWithAgent(SFTP $connection): void\n    {\n        $agent = new Agent();\n\n        if ( ! $connection->login($this->username, $agent)) {\n            throw UnableToAuthenticate::withSshAgent($connection->getLastError());\n        }\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV3/SftpConnectionProviderTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse phpseclib3\\Net\\SFTP;\nuse PHPUnit\\Framework\\TestCase;\nuse Throwable;\n\nuse function base64_decode;\nuse function class_exists;\nuse function explode;\nuse function getenv;\nuse function hash;\nuse function implode;\nuse function is_a;\nuse function sleep;\nuse function str_split;\n\n/**\n * @group sftp\n * @group sftp-connection\n * @group phpseclib3\n */\nclass SftpConnectionProviderTest extends TestCase\n{\n    const KEX_ACCEPTED_BY_DEFAULT_OPENSSH_BUT_DISABLED_IN_EDDSA_ONLY = 'diffie-hellman-group14-sha256';\n\n    public static function setUpBeforeClass(): void\n    {\n        if ( ! class_exists(SFTP::class)) {\n            self::markTestIncomplete(\"No phpseclib v3 installed\");\n        }\n    }\n\n    /**\n     * @test\n     */\n    public function giving_up_after_5_connection_failures(): void\n    {\n        $this->expectException(UnableToConnectToSftpHost::class);\n\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'password' => 'pass',\n                'port' => 2222,\n                'timeout' => 10,\n                'connectivityChecker' => new FixatedConnectivityChecker(5),\n            ]\n        );\n\n        $provider->provideConnection();\n    }\n\n    /**\n     * @test\n     */\n    public function trying_until_5_tries(): void\n    {\n        $provider = SftpConnectionProvider::fromArray([\n            'host' => 'localhost',\n            'username' => 'foo',\n            'password' => 'pass',\n            'port' => 2222,\n            'timeout' => 10,\n            'connectivityChecker' => new FixatedConnectivityChecker(4),\n        ]);\n        $connection = $provider->provideConnection();\n        $sameConnection = $provider->provideConnection();\n\n        $this->assertInstanceOf(SFTP::class, $connection);\n        $this->assertSame($connection, $sameConnection);\n    }\n\n    /**\n     * @test\n     */\n    public function authenticating_with_a_private_key(): void\n    {\n        $provider = SftpConnectionProvider::fromArray([\n            'host' => 'localhost',\n            'username' => 'bar',\n            'privateKey' => __DIR__ . '/../../test_files/sftp/id_rsa',\n            'passphrase' => 'secret',\n            'port' => 2222,\n        ]);\n\n        $connection = null;\n        $this->runWithRetries(function () use (&$connection, $provider) {\n            $connection = $provider->provideConnection();\n        });\n        $this->assertInstanceOf(SFTP::class, $connection);\n    }\n\n    /**\n     * @test\n     */\n    public function authenticating_with_an_invalid_private_key(): void\n    {\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'bar',\n                'privateKey' => __DIR__ . '/../../test_files/sftp/users.conf',\n                'port' => 2222,\n            ]\n        );\n\n        $this->expectException(UnableToLoadPrivateKey::class);\n\n        $this->runWithRetries(fn () => $provider->provideConnection(), UnableToLoadPrivateKey::class);\n    }\n\n    /**\n     * @test\n     */\n    public function authenticating_with_an_ssh_agent(): void\n    {\n        if (getenv('COMPOSER_OPTS') === false) {\n            $this->markTestSkipped('Test is not run locally');\n        }\n\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'bar',\n                'useAgent' => true,\n                'port' => 2222,\n            ]\n        );\n\n        $connection = null;\n        $this->runWithRetries(function () use ($provider, &$connection) {\n            $connection = $provider->provideConnection();\n        });\n        $this->assertInstanceOf(SFTP::class, $connection);\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_authenticating_with_an_ssh_agent(): void\n    {\n        $this->expectException(UnableToAuthenticate::class);\n\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'useAgent' => true,\n                'port' => 2222,\n            ]\n        );\n\n        $provider->provideConnection();\n    }\n\n    /**\n     * @test\n     */\n    public function authenticating_with_a_private_key_and_falling_back_to_password(): void\n    {\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'password' => 'pass',\n                'privateKey' => __DIR__ . '/../../test_files/sftp/id_rsa',\n                'passphrase' => 'secret',\n                'port' => 2222,\n            ]\n        );\n\n        $connection = null;\n        $this->runWithRetries(function () use ($provider, &$connection) {\n            $connection = $provider->provideConnection();\n        });\n        $this->assertInstanceOf(SFTP::class, $connection);\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_authenticate_with_a_private_key(): void\n    {\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'privateKey' => __DIR__ . '/../../test_files/sftp/unknown.key',\n                'passphrase' => 'secret',\n                'port' => 2222,\n            ]\n        );\n\n        $this->expectExceptionObject(UnableToAuthenticate::withPrivateKey());\n        $this->runWithRetries(fn () => $provider->provideConnection(), UnableToAuthenticate::class);\n    }\n\n    /**\n     * @test\n     */\n    public function verifying_a_fingerprint(): void\n    {\n        $key = file_get_contents(__DIR__ . '/../../test_files/sftp/ssh_host_ed25519_key.pub');\n        $fingerPrint = $this->computeFingerPrint($key);\n\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'password' => 'pass',\n                'port' => 2222,\n                'hostFingerprint' => $fingerPrint,\n            ]\n        );\n\n        $connection = null;\n        $this->runWithRetries(function () use ($provider, &$connection) {\n            $connection = $provider->provideConnection();\n        });\n        $this->assertInstanceOf(SFTP::class, $connection);\n    }\n\n    /**\n     * @test\n     */\n    public function providing_an_invalid_fingerprint(): void\n    {\n        $this->expectException(UnableToEstablishAuthenticityOfHost::class);\n\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'password' => 'pass',\n                'port' => 2222,\n                'hostFingerprint' => 'invalid:fingerprint',\n            ]\n        );\n        $this->runWithRetries(fn () => $provider->provideConnection(), UnableToEstablishAuthenticityOfHost::class);\n    }\n\n    /**\n     * @test\n     */\n    public function providing_an_invalid_password(): void\n    {\n        $this->expectException(UnableToAuthenticate::class);\n\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'password' => 'lol',\n                'port' => 2222,\n            ]\n        );\n\n        $this->runWithRetries(fn () => $provider->provideConnection(), UnableToAuthenticate::class);\n    }\n\n    /**\n     * @test\n     */\n    public function retries_several_times_until_failure(): void\n    {\n        $connectivityChecker = new class implements ConnectivityChecker {\n            /** @var int */\n            public $calls = 0;\n\n            public function isConnected(SFTP $connection): bool\n            {\n                ++$this->calls;\n\n                return false;\n            }\n        };\n\n        $maxTries = 2;\n\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'bar',\n                'privateKey' => __DIR__ . '/../../test_files/sftp/id_rsa',\n                'passphrase' => 'secret',\n                'port' => 8222,\n                'maxTries' => $maxTries,\n                'timeout' => 1,\n                'connectivityChecker' => $connectivityChecker,\n            ]\n        );\n\n        $this->expectException(UnableToConnectToSftpHost::class);\n\n        try {\n            $provider->provideConnection();\n        } finally {\n            self::assertSame($maxTries + 1, $connectivityChecker->calls);\n        }\n    }\n\n    /**\n     * @test\n     */\n    public function authenticate_with_supported_preferred_kex_algorithm_succeeds(): void\n    {\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'password' => 'pass',\n                'port' => 2222,\n                'preferredAlgorithms' => [\n                    'kex' => [self::KEX_ACCEPTED_BY_DEFAULT_OPENSSH_BUT_DISABLED_IN_EDDSA_ONLY],\n                ],\n            ]\n        );\n\n        $this->runWithRetries(fn () => $this->assertInstanceOf(SFTP::class, $provider->provideConnection()));\n\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'password' => 'pass',\n                'port' => 2223,\n                'preferredAlgorithms' => [\n                    'kex' => ['curve25519-sha256'],\n                ],\n            ]\n        );\n\n        $this->runWithRetries(fn () => $this->assertInstanceOf(SFTP::class, $provider->provideConnection()));\n    }\n\n    /**\n     * @test\n     */\n    public function authenticate_with_unsupported_preferred_kex_algorithm_failes(): void\n    {\n        $provider = SftpConnectionProvider::fromArray(\n            [\n                'host' => 'localhost',\n                'username' => 'foo',\n                'password' => 'pass',\n                'port' => 2223,\n                'preferredAlgorithms' => [\n                    'kex' => [self::KEX_ACCEPTED_BY_DEFAULT_OPENSSH_BUT_DISABLED_IN_EDDSA_ONLY],\n                ],\n            ]\n        );\n\n        $this->expectException(UnableToConnectToSftpHost::class);\n\n        $provider->provideConnection();\n    }\n\n    private function computeFingerPrint(string $publicKey): string\n    {\n        $content = explode(' ', $publicKey, 3);\n        $algo = $content[0] === 'ssh-rsa' ? 'md5' : 'sha512';\n\n        return implode(':', str_split(hash($algo, base64_decode($content[1])), 2));\n    }\n\n    /**\n     * @param class-string<Throwable>|null $expected\n     *\n     * @throws Throwable\n     */\n    public function runWithRetries(callable $scenario, ?string $expected = null): void\n    {\n        $tries = 0;\n        start:\n\n        try {\n            $scenario();\n        } catch (Throwable $exception) {\n            if (($expected === null || is_a($exception, $expected) === false) && $tries < 10) {\n                $tries++;\n                sleep($tries);\n                goto start;\n            }\n\n            throw $exception;\n        }\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV3/SftpStub.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse phpseclib3\\Net\\SFTP;\n\n/**\n * @internal This is only used for testing purposes.\n */\nclass SftpStub extends SFTP\n{\n    /**\n     * @var array<string,bool>\n     */\n    private array $tripWires = [];\n\n    public function failOnChmod(string $filename): void\n    {\n        $key = $this->formatTripKey('chmod', $filename);\n        $this->tripWires[$key] = true;\n    }\n\n    /**\n     * @param int    $mode\n     * @param string $filename\n     * @param bool   $recursive\n     *\n     * @return bool|mixed\n     */\n    public function chmod($mode, $filename, $recursive = false)\n    {\n        $key = $this->formatTripKey('chmod', $filename);\n        $shouldTrip = $this->tripWires[$key] ?? false;\n\n        if ($shouldTrip) {\n            unset($this->tripWires[$key]);\n\n            return false;\n        }\n\n        return parent::chmod($mode, $filename, $recursive);\n    }\n\n    public function failOnPut(string $filename): void\n    {\n        $key = $this->formatTripKey('put', $filename);\n        $this->tripWires[$key] = true;\n    }\n\n    /**\n     * @param string          $remote_file\n     * @param resource|string $data\n     * @param int             $mode\n     * @param int             $start\n     * @param int             $local_start\n     * @param null            $progressCallback\n     *\n     * @return bool\n     */\n    public function put(\n        $remote_file,\n        $data,\n        $mode = self::SOURCE_STRING,\n        $start = -1,\n        $local_start = -1,\n        $progressCallback = null\n    ) {\n        $key = $this->formatTripKey('put', $remote_file);\n        $shouldTrip = $this->tripWires[$key] ?? false;\n\n        if ($shouldTrip) {\n            return false;\n        }\n\n        return parent::put($remote_file, $data, $mode, $start, $local_start, $progressCallback);\n    }\n\n    /**\n     * @param array<int,mixed> $arguments\n     *\n     * @return string\n     */\n    private function formatTripKey(...$arguments): string\n    {\n        $key = '';\n\n        foreach ($arguments as $argument) {\n            $key .= var_export($argument, true);\n        }\n\n        return $key;\n    }\n\n    public function resetTripWires(): void\n    {\n        $this->tripWires = [];\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV3/SimpleConnectivityChecker.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse phpseclib3\\Net\\SFTP;\nuse Throwable;\n\nclass SimpleConnectivityChecker implements ConnectivityChecker\n{\n    public function __construct(\n        private bool $usePing = false,\n    ) {\n    }\n\n    public static function create(): SimpleConnectivityChecker\n    {\n        return new SimpleConnectivityChecker();\n    }\n\n    public function withUsingPing(bool $usePing): SimpleConnectivityChecker\n    {\n        $clone = clone $this;\n        $clone->usePing = $usePing;\n\n        return $clone;\n    }\n\n    public function isConnected(SFTP $connection): bool\n    {\n        if ( ! $connection->isConnected()) {\n            return false;\n        }\n\n        if ( ! $this->usePing) {\n            return true;\n        }\n\n        try {\n            return $connection->ping();\n        } catch (Throwable) {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV3/StubSftpConnectionProvider.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse phpseclib3\\Net\\SFTP;\n\nclass StubSftpConnectionProvider implements ConnectionProvider\n{\n    /**\n     * @var SftpStub|null\n     */\n    public $connection;\n\n    public function __construct(\n        private string $host,\n        private string $username,\n        private ?string $password = null,\n        private int $port = 22\n    ) {\n    }\n\n    public function disconnect(): void\n    {\n        if ($this->connection) {\n            $this->connection->disconnect();\n        }\n    }\n\n    public function provideConnection(): SFTP\n    {\n        if ( ! $this->connection instanceof SFTP || ! $this->connection->isConnected()) {\n            $connection = new SftpStub($this->host, $this->port);\n            $connection->login($this->username, $this->password);\n\n            $this->connection = $connection;\n        }\n\n        return $this->connection;\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV3/UnableToAuthenticate.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse League\\Flysystem\\FilesystemException;\nuse RuntimeException;\n\nclass UnableToAuthenticate extends RuntimeException implements FilesystemException\n{\n    private ?string $connectionError;\n\n    public function __construct(string $message, ?string $lastError = null)\n    {\n        parent::__construct($message);\n        $this->connectionError = $lastError;\n    }\n\n    public static function withPassword(?string $lastError = null): UnableToAuthenticate\n    {\n        return new UnableToAuthenticate('Unable to authenticate using a password.', $lastError);\n    }\n\n    public static function withPrivateKey(?string $lastError = null): UnableToAuthenticate\n    {\n        return new UnableToAuthenticate('Unable to authenticate using a private key.', $lastError);\n    }\n\n    public static function withSshAgent(?string $lastError = null): UnableToAuthenticate\n    {\n        return new UnableToAuthenticate('Unable to authenticate using an SSH agent.', $lastError);\n    }\n\n    public function connectionError(): ?string\n    {\n        return $this->connectionError;\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV3/UnableToConnectToSftpHost.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse League\\Flysystem\\FilesystemException;\nuse RuntimeException;\nuse Throwable;\n\nclass UnableToConnectToSftpHost extends RuntimeException implements FilesystemException\n{\n    public static function atHostname(string $host, ?Throwable $previous = null): UnableToConnectToSftpHost\n    {\n        return new UnableToConnectToSftpHost(\"Unable to connect to host: $host\", 0, $previous);\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV3/UnableToEstablishAuthenticityOfHost.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse League\\Flysystem\\FilesystemException;\nuse RuntimeException;\n\nclass UnableToEstablishAuthenticityOfHost extends RuntimeException implements FilesystemException\n{\n    public static function becauseTheAuthenticityCantBeEstablished(string $host): UnableToEstablishAuthenticityOfHost\n    {\n        return new UnableToEstablishAuthenticityOfHost(\"The authenticity of host $host can't be established.\");\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV3/UnableToLoadPrivateKey.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\PhpseclibV3;\n\nuse League\\Flysystem\\FilesystemException;\nuse RuntimeException;\nuse Throwable;\n\nclass UnableToLoadPrivateKey extends RuntimeException implements FilesystemException\n{\n    public function __construct(?string $message = 'Unable to load private key.', ?Throwable $previous = null)\n    {\n        parent::__construct($message ?? 'Unable to load private key.', 0, $previous);\n    }\n}\n"
  },
  {
    "path": "src/PhpseclibV3/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-sftp-v3\",\n    \"description\": \"SFTP filesystem adapter for Flysystem.\",\n    \"keywords\": [\"flysystem\", \"filesystem\", \"sftp\", \"files\", \"file\"],\n    \"autoload\": {\n        \"psr-4\": {\n                \"League\\\\Flysystem\\\\PhpseclibV3\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"league/flysystem\": \"^3.0.14\",\n        \"league/mime-type-detection\": \"^1.0.0\",\n        \"phpseclib/phpseclib\": \"^3.0\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ]\n}\n"
  },
  {
    "path": "src/PortableVisibilityGuard.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nfinal class PortableVisibilityGuard\n{\n    public static function guardAgainstInvalidInput(string $visibility): void\n    {\n        if ($visibility !== Visibility::PUBLIC && $visibility !== Visibility::PRIVATE) {\n            $className = Visibility::class;\n            throw InvalidVisibilityProvided::withVisibility(\n                $visibility,\n                \"either {$className}::PUBLIC or {$className}::PRIVATE\"\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/ProxyArrayAccessToProperties.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\n\n/**\n * @internal\n */\ntrait ProxyArrayAccessToProperties\n{\n    private function formatPropertyName(string $offset): string\n    {\n        return str_replace('_', '', lcfirst(ucwords($offset, '_')));\n    }\n\n    /**\n     * @param mixed $offset\n     *\n     * @return bool\n     */\n    public function offsetExists($offset): bool\n    {\n        $property = $this->formatPropertyName((string) $offset);\n\n        return isset($this->{$property});\n    }\n\n    /**\n     * @param mixed $offset\n     *\n     * @return mixed\n     */\n    #[\\ReturnTypeWillChange]\n    public function offsetGet($offset)\n    {\n        $property = $this->formatPropertyName((string) $offset);\n\n        return $this->{$property};\n    }\n\n    /**\n     * @param mixed $offset\n     * @param mixed $value\n     */\n    #[\\ReturnTypeWillChange]\n    public function offsetSet($offset, $value): void\n    {\n        throw new RuntimeException('Properties can not be manipulated');\n    }\n\n    /**\n     * @param mixed $offset\n     */\n    #[\\ReturnTypeWillChange]\n    public function offsetUnset($offset): void\n    {\n        throw new RuntimeException('Properties can not be manipulated');\n    }\n}\n"
  },
  {
    "path": "src/ReadOnly/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n**/*Test.php export-ignore\nREADME.md export-ignore\n"
  },
  {
    "path": "src/ReadOnly/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/ReadOnly/LICENSE",
    "content": "Copyright (c) 2013-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/ReadOnly/README.md",
    "content": "## Sub-split of Flysystem for read-only adapter decoration.\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n\n```bash\ncomposer require league/flysystem-read-only\n```\n\nView the [documentation](https://flysystem.thephpleague.com/docs/adapter/read-only/).\n"
  },
  {
    "path": "src/ReadOnly/ReadOnlyFilesystemAdapter.php",
    "content": "<?php\n\nnamespace League\\Flysystem\\ReadOnly;\n\nuse DateTimeInterface;\nuse League\\Flysystem\\CalculateChecksumFromStream;\nuse League\\Flysystem\\ChecksumProvider;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DecoratedAdapter;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToGeneratePublicUrl;\nuse League\\Flysystem\\UnableToGenerateTemporaryUrl;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UrlGeneration\\PublicUrlGenerator;\nuse League\\Flysystem\\UrlGeneration\\TemporaryUrlGenerator;\n\nclass ReadOnlyFilesystemAdapter extends DecoratedAdapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider, TemporaryUrlGenerator\n{\n    use CalculateChecksumFromStream;\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        throw UnableToWriteFile::atLocation($path, 'This is a readonly adapter.');\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        throw UnableToWriteFile::atLocation($path, 'This is a readonly adapter.');\n    }\n\n    public function delete(string $path): void\n    {\n        throw UnableToDeleteFile::atLocation($path, 'This is a readonly adapter.');\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        throw UnableToDeleteDirectory::atLocation($path, 'This is a readonly adapter.');\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        throw UnableToCreateDirectory::atLocation($path, 'This is a readonly adapter.');\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        throw UnableToSetVisibility::atLocation($path, 'This is a readonly adapter.');\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        throw new UnableToMoveFile(\"Unable to move file from $source to $destination as this is a readonly adapter.\");\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        throw new UnableToCopyFile(\"Unable to copy file from $source to $destination as this is a readonly adapter.\");\n    }\n\n    public function publicUrl(string $path, Config $config): string\n    {\n        if ( ! $this->adapter instanceof PublicUrlGenerator) {\n            throw UnableToGeneratePublicUrl::noGeneratorConfigured($path);\n        }\n\n        return $this->adapter->publicUrl($path, $config);\n    }\n\n    public function checksum(string $path, Config $config): string\n    {\n        if ($this->adapter instanceof ChecksumProvider) {\n            return $this->adapter->checksum($path, $config);\n        }\n\n        return $this->calculateChecksumFromStream($path, $config);\n    }\n\n    public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string\n    {\n        if ( ! $this->adapter instanceof TemporaryUrlGenerator) {\n            throw UnableToGenerateTemporaryUrl::noGeneratorConfigured($path);\n        }\n\n        return $this->adapter->temporaryUrl($path, $expiresAt, $config);\n    }\n}\n"
  },
  {
    "path": "src/ReadOnly/ReadOnlyFilesystemAdapterTest.php",
    "content": "<?php\n\nnamespace League\\Flysystem\\ReadOnly;\n\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\InMemory\\InMemoryFilesystemAdapter;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToGeneratePublicUrl;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UrlGeneration\\PublicUrlGenerator;\nuse PHPUnit\\Framework\\TestCase;\n\nuse function ltrim;\n\nclass ReadOnlyFilesystemAdapterTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function can_perform_read_operations(): void\n    {\n        $adapter = $this->realAdapter();\n        $adapter->write('foo/bar.txt', 'content', new Config());\n\n        $adapter = new ReadOnlyFilesystemAdapter($adapter);\n\n        $this->assertTrue($adapter->fileExists('foo/bar.txt'));\n        $this->assertTrue($adapter->directoryExists('foo'));\n        $this->assertSame('content', $adapter->read('foo/bar.txt'));\n        $this->assertSame('content', \\stream_get_contents($adapter->readStream('foo/bar.txt')));\n        $this->assertInstanceOf(FileAttributes::class, $adapter->visibility('foo/bar.txt'));\n        $this->assertInstanceOf(FileAttributes::class, $adapter->mimeType('foo/bar.txt'));\n        $this->assertInstanceOf(FileAttributes::class, $adapter->lastModified('foo/bar.txt'));\n        $this->assertInstanceOf(FileAttributes::class, $adapter->fileSize('foo/bar.txt'));\n        $this->assertCount(1, iterator_to_array($adapter->listContents('foo', true)));\n    }\n\n    /**\n     * @test\n     */\n    public function cannot_write_stream(): void\n    {\n        $adapter = new ReadOnlyFilesystemAdapter($this->realAdapter());\n\n        $this->expectException(UnableToWriteFile::class);\n\n        // @phpstan-ignore-next-line\n        $adapter->writeStream('foo', 'content', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function cannot_write(): void\n    {\n        $adapter = new ReadOnlyFilesystemAdapter($this->realAdapter());\n\n        $this->expectException(UnableToWriteFile::class);\n\n        $adapter->write('foo', 'content', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function cannot_delete_file(): void\n    {\n        $adapter = $this->realAdapter();\n        $adapter->write('foo', 'content', new Config());\n\n        $adapter = new ReadOnlyFilesystemAdapter($adapter);\n\n        $this->expectException(UnableToDeleteFile::class);\n\n        $adapter->delete('foo');\n    }\n\n    /**\n     * @test\n     */\n    public function cannot_delete_directory(): void\n    {\n        $adapter = $this->realAdapter();\n        $adapter->createDirectory('foo', new Config());\n\n        $adapter = new ReadOnlyFilesystemAdapter($adapter);\n\n        $this->expectException(UnableToDeleteDirectory::class);\n\n        $adapter->deleteDirectory('foo');\n    }\n\n    /**\n     * @test\n     */\n    public function cannot_create_directory(): void\n    {\n        $adapter = new ReadOnlyFilesystemAdapter($this->realAdapter());\n\n        $this->expectException(UnableToCreateDirectory::class);\n\n        $adapter->createDirectory('foo', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function cannot_set_visibility(): void\n    {\n        $adapter = $this->realAdapter();\n        $adapter->write('foo', 'content', new Config());\n\n        $adapter = new ReadOnlyFilesystemAdapter($adapter);\n\n        $this->expectException(UnableToSetVisibility::class);\n\n        $adapter->setVisibility('foo', 'private');\n    }\n\n    /**\n     * @test\n     */\n    public function cannot_move(): void\n    {\n        $adapter = $this->realAdapter();\n        $adapter->write('foo', 'content', new Config());\n\n        $adapter = new ReadOnlyFilesystemAdapter($adapter);\n\n        $this->expectException(UnableToMoveFile::class);\n\n        $adapter->move('foo', 'bar', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function cannot_copy(): void\n    {\n        $adapter = $this->realAdapter();\n        $adapter->write('foo', 'content', new Config());\n\n        $adapter = new ReadOnlyFilesystemAdapter($adapter);\n\n        $this->expectException(UnableToCopyFile::class);\n\n        $adapter->copy('foo', 'bar', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function generating_a_public_url(): void\n    {\n        $adapter = new class() extends InMemoryFilesystemAdapter implements PublicUrlGenerator {\n            public function publicUrl(string $path, Config $config): string\n            {\n                return 'memory://' . ltrim($path, '/');\n            }\n        };\n        $readOnlyAdapter = new ReadOnlyFilesystemAdapter($adapter);\n\n        $url = $readOnlyAdapter->publicUrl('/path.txt', new Config());\n\n        self::assertEquals('memory://path.txt', $url);\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_generate_a_public_url(): void\n    {\n        $adapter = new ReadOnlyFilesystemAdapter(new InMemoryFilesystemAdapter());\n\n        $this->expectException(UnableToGeneratePublicUrl::class);\n\n        $adapter->publicUrl('/path.txt', new Config());\n    }\n\n    private function realAdapter(): InMemoryFilesystemAdapter\n    {\n        return new InMemoryFilesystemAdapter();\n    }\n}\n"
  },
  {
    "path": "src/ReadOnly/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-read-only\",\n    \"description\": \"Read-only filesystem adapter for Flysystem.\",\n    \"keywords\": [\"flysystem\", \"filesystem\", \"read-only\", \"read\", \"only\"],\n    \"type\": \"library\",\n    \"prefer-stable\": true,\n    \"autoload\": {\n        \"psr-4\": {\n            \"League\\\\Flysystem\\\\ReadOnly\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"league/flysystem\": \"^3.10.0\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ]\n}\n"
  },
  {
    "path": "src/ResolveIdenticalPathConflict.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nclass ResolveIdenticalPathConflict\n{\n    public const IGNORE = 'ignore';\n    public const FAIL = 'fail';\n    public const TRY = 'try';\n}\n"
  },
  {
    "path": "src/StorageAttributes.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse ArrayAccess;\nuse JsonSerializable;\n\ninterface StorageAttributes extends JsonSerializable, ArrayAccess\n{\n    public const ATTRIBUTE_PATH = 'path';\n    public const ATTRIBUTE_TYPE = 'type';\n    public const ATTRIBUTE_FILE_SIZE = 'file_size';\n    public const ATTRIBUTE_VISIBILITY = 'visibility';\n    public const ATTRIBUTE_LAST_MODIFIED = 'last_modified';\n    public const ATTRIBUTE_MIME_TYPE = 'mime_type';\n    public const ATTRIBUTE_EXTRA_METADATA = 'extra_metadata';\n\n    public const TYPE_FILE = 'file';\n    public const TYPE_DIRECTORY = 'dir';\n\n    public function path(): string;\n\n    public function type(): string;\n\n    public function visibility(): ?string;\n\n    public function lastModified(): ?int;\n\n    public static function fromArray(array $attributes): StorageAttributes;\n\n    public function isFile(): bool;\n\n    public function isDir(): bool;\n\n    public function withPath(string $path): StorageAttributes;\n\n    public function extraMetadata(): array;\n}\n"
  },
  {
    "path": "src/SymbolicLinkEncountered.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\n\nfinal class SymbolicLinkEncountered extends RuntimeException implements FilesystemException\n{\n    private string $location;\n\n    public function location(): string\n    {\n        return $this->location;\n    }\n\n    public static function atLocation(string $pathName): SymbolicLinkEncountered\n    {\n        $e = new static(\"Unsupported symbolic link encountered at location $pathName\");\n        $e->location = $pathName;\n\n        return $e;\n    }\n}\n"
  },
  {
    "path": "src/UnableToCheckDirectoryExistence.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nclass UnableToCheckDirectoryExistence extends UnableToCheckExistence\n{\n    public function operation(): string\n    {\n        return FilesystemOperationFailed::OPERATION_DIRECTORY_EXISTS;\n    }\n}\n"
  },
  {
    "path": "src/UnableToCheckExistence.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\nuse Throwable;\n\nclass UnableToCheckExistence extends RuntimeException implements FilesystemOperationFailed\n{\n    final public function __construct(string $message = \"\", int $code = 0, ?Throwable $previous = null)\n    {\n        parent::__construct($message, $code, $previous);\n    }\n\n    public static function forLocation(string $path, ?Throwable $exception = null): static\n    {\n        return new static(\"Unable to check existence for: {$path}\", 0, $exception);\n    }\n\n    public function operation(): string\n    {\n        return FilesystemOperationFailed::OPERATION_EXISTENCE_CHECK;\n    }\n}\n"
  },
  {
    "path": "src/UnableToCheckFileExistence.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nclass UnableToCheckFileExistence extends UnableToCheckExistence\n{\n    public function operation(): string\n    {\n        return FilesystemOperationFailed::OPERATION_FILE_EXISTS;\n    }\n}\n"
  },
  {
    "path": "src/UnableToCopyFile.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\nuse Throwable;\n\nfinal class UnableToCopyFile extends RuntimeException implements FilesystemOperationFailed\n{\n    /**\n     * @var string\n     */\n    private $source;\n\n    /**\n     * @var string\n     */\n    private $destination;\n\n    public function source(): string\n    {\n        return $this->source;\n    }\n\n    public function destination(): string\n    {\n        return $this->destination;\n    }\n\n    public static function fromLocationTo(\n        string $sourcePath,\n        string $destinationPath,\n        ?Throwable $previous = null\n    ): UnableToCopyFile {\n        $e = new static(\"Unable to copy file from $sourcePath to $destinationPath\", 0 , $previous);\n        $e->source = $sourcePath;\n        $e->destination = $destinationPath;\n\n        return $e;\n    }\n\n    public static function sourceAndDestinationAreTheSame(string $source, string $destination): UnableToCopyFile\n    {\n        return UnableToCopyFile::because('Source and destination are the same', $source, $destination);\n    }\n\n    public static function because(string $reason, string $sourcePath, string $destinationPath): UnableToCopyFile\n    {\n        $e = new static(\"Unable to copy file from $sourcePath to $destinationPath, because $reason\");\n        $e->source = $sourcePath;\n        $e->destination = $destinationPath;\n\n        return $e;\n    }\n\n    public function operation(): string\n    {\n        return FilesystemOperationFailed::OPERATION_COPY;\n    }\n}\n"
  },
  {
    "path": "src/UnableToCreateDirectory.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\nuse Throwable;\n\nfinal class UnableToCreateDirectory extends RuntimeException implements FilesystemOperationFailed\n{\n    private string $location;\n    private string $reason = '';\n\n    public static function atLocation(string $dirname, string $errorMessage = '', ?Throwable $previous = null): UnableToCreateDirectory\n    {\n        $message = \"Unable to create a directory at {$dirname}. {$errorMessage}\";\n        $e = new static(rtrim($message), 0, $previous);\n        $e->location = $dirname;\n        $e->reason = $errorMessage;\n\n        return $e;\n    }\n\n    public static function dueToFailure(string $dirname, Throwable $previous): UnableToCreateDirectory\n    {\n        $reason = $previous instanceof UnableToCreateDirectory ? $previous->reason() : '';\n        $message = \"Unable to create a directory at $dirname. $reason\";\n        $e = new static(rtrim($message), 0, $previous);\n        $e->location = $dirname;\n        $e->reason = $reason ?: $message;\n\n        return $e;\n    }\n\n    public function operation(): string\n    {\n        return FilesystemOperationFailed::OPERATION_CREATE_DIRECTORY;\n    }\n\n    public function reason(): string\n    {\n        return $this->reason;\n    }\n\n    public function location(): string\n    {\n        return $this->location;\n    }\n}\n"
  },
  {
    "path": "src/UnableToDeleteDirectory.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\nuse Throwable;\n\nfinal class UnableToDeleteDirectory extends RuntimeException implements FilesystemOperationFailed\n{\n    /**\n     * @var string\n     */\n    private $location = '';\n\n    /**\n     * @var string\n     */\n    private $reason;\n\n    public static function atLocation(\n        string $location,\n        string $reason = '',\n        ?Throwable $previous = null\n    ): UnableToDeleteDirectory {\n        $e = new static(rtrim(\"Unable to delete directory located at: {$location}. {$reason}\"), 0, $previous);\n        $e->location = $location;\n        $e->reason = $reason;\n\n        return $e;\n    }\n\n    public function operation(): string\n    {\n        return FilesystemOperationFailed::OPERATION_DELETE_DIRECTORY;\n    }\n\n    public function reason(): string\n    {\n        return $this->reason;\n    }\n\n    public function location(): string\n    {\n        return $this->location;\n    }\n}\n"
  },
  {
    "path": "src/UnableToDeleteFile.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\nuse Throwable;\n\nfinal class UnableToDeleteFile extends RuntimeException implements FilesystemOperationFailed\n{\n    /**\n     * @var string\n     */\n    private $location = '';\n\n    /**\n     * @var string\n     */\n    private $reason;\n\n    public static function atLocation(string $location, string $reason = '', ?Throwable $previous = null): UnableToDeleteFile\n    {\n        $e = new static(rtrim(\"Unable to delete file located at: {$location}. {$reason}\"), 0, $previous);\n        $e->location = $location;\n        $e->reason = $reason;\n\n        return $e;\n    }\n\n    public function operation(): string\n    {\n        return FilesystemOperationFailed::OPERATION_DELETE;\n    }\n\n    public function reason(): string\n    {\n        return $this->reason;\n    }\n\n    public function location(): string\n    {\n        return $this->location;\n    }\n}\n"
  },
  {
    "path": "src/UnableToGeneratePublicUrl.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\nuse Throwable;\n\nfinal class UnableToGeneratePublicUrl extends RuntimeException implements FilesystemException\n{\n    public function __construct(string $reason, string $path, ?Throwable $previous = null)\n    {\n        parent::__construct(\"Unable to generate public url for $path: $reason\", 0, $previous);\n    }\n\n    public static function dueToError(string $path, Throwable $exception): static\n    {\n        return new static($exception->getMessage(), $path, $exception);\n    }\n\n    public static function noGeneratorConfigured(string $path, string $extraReason = ''): static\n    {\n        return new static('No generator was configured ' . $extraReason, $path);\n    }\n}\n"
  },
  {
    "path": "src/UnableToGenerateTemporaryUrl.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\nuse Throwable;\n\nfinal class UnableToGenerateTemporaryUrl extends RuntimeException implements FilesystemException\n{\n    public function __construct(string $reason, string $path, ?Throwable $previous = null)\n    {\n        parent::__construct(\"Unable to generate temporary url for $path: $reason\", 0, $previous);\n    }\n\n    public static function dueToError(string $path, Throwable $exception): static\n    {\n        return new static($exception->getMessage(), $path, $exception);\n    }\n\n    public static function noGeneratorConfigured(string $path, string $extraReason = ''): static\n    {\n        return new static('No generator was configured ' . $extraReason, $path);\n    }\n}\n"
  },
  {
    "path": "src/UnableToListContents.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\nuse Throwable;\n\nfinal class UnableToListContents extends RuntimeException implements FilesystemOperationFailed\n{\n    public static function atLocation(string $location, bool $deep, Throwable $previous): UnableToListContents\n    {\n        $message = \"Unable to list contents for '$location', \" . ($deep ? 'deep' : 'shallow') . \" listing\\n\\n\"\n            . 'Reason: ' . $previous->getMessage();\n\n        return new UnableToListContents($message, 0, $previous);\n    }\n\n    public function operation(): string\n    {\n        return self::OPERATION_LIST_CONTENTS;\n    }\n}\n"
  },
  {
    "path": "src/UnableToMountFilesystem.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse LogicException;\n\nclass UnableToMountFilesystem extends LogicException implements FilesystemException\n{\n    /**\n     * @param mixed $key\n     */\n    public static function becauseTheKeyIsNotValid($key): UnableToMountFilesystem\n    {\n        return new UnableToMountFilesystem(\n            'Unable to mount filesystem, key was invalid. String expected, received: ' . gettype($key)\n        );\n    }\n\n    /**\n     * @param mixed $filesystem\n     */\n    public static function becauseTheFilesystemWasNotValid($filesystem): UnableToMountFilesystem\n    {\n        $received = is_object($filesystem) ? get_class($filesystem) : gettype($filesystem);\n\n        return new UnableToMountFilesystem(\n            'Unable to mount filesystem, filesystem was invalid. Instance of ' . FilesystemOperator::class . ' expected, received: ' . $received\n        );\n    }\n}\n"
  },
  {
    "path": "src/UnableToMoveFile.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\nuse Throwable;\n\nfinal class UnableToMoveFile extends RuntimeException implements FilesystemOperationFailed\n{\n    /**\n     * @var string\n     */\n    private $source;\n\n    /**\n     * @var string\n     */\n    private $destination;\n\n    public static function sourceAndDestinationAreTheSame(string $source, string $destination): UnableToMoveFile\n    {\n        return UnableToMoveFile::because('Source and destination are the same', $source, $destination);\n    }\n\n    public function source(): string\n    {\n        return $this->source;\n    }\n\n    public function destination(): string\n    {\n        return $this->destination;\n    }\n\n    public static function fromLocationTo(\n        string $sourcePath,\n        string $destinationPath,\n        ?Throwable $previous = null\n    ): UnableToMoveFile {\n        $message = $previous?->getMessage() ?? \"Unable to move file from $sourcePath to $destinationPath\";\n        $e = new static($message, 0, $previous);\n        $e->source = $sourcePath;\n        $e->destination = $destinationPath;\n\n        return $e;\n    }\n\n    public static function because(\n        string $reason,\n        string $sourcePath,\n        string $destinationPath,\n    ): UnableToMoveFile {\n        $message = \"Unable to move file from $sourcePath to $destinationPath, because $reason\";\n        $e = new static($message);\n        $e->source = $sourcePath;\n        $e->destination = $destinationPath;\n\n        return $e;\n    }\n\n    public function operation(): string\n    {\n        return FilesystemOperationFailed::OPERATION_MOVE;\n    }\n}\n"
  },
  {
    "path": "src/UnableToProvideChecksum.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\nuse Throwable;\n\nfinal class UnableToProvideChecksum extends RuntimeException implements FilesystemException\n{\n    public function __construct(string $reason, string $path, ?Throwable $previous = null)\n    {\n        parent::__construct(\"Unable to get checksum for $path: $reason\", 0, $previous);\n    }\n}\n"
  },
  {
    "path": "src/UnableToReadFile.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\nuse Throwable;\n\nfinal class UnableToReadFile extends RuntimeException implements FilesystemOperationFailed\n{\n    /**\n     * @var string\n     */\n    private $location = '';\n\n    /**\n     * @var string\n     */\n    private $reason = '';\n\n    public static function fromLocation(string $location, string $reason = '', ?Throwable $previous = null): UnableToReadFile\n    {\n        $e = new static(rtrim(\"Unable to read file from location: {$location}. {$reason}\"), 0, $previous);\n        $e->location = $location;\n        $e->reason = $reason;\n\n        return $e;\n    }\n\n    public function operation(): string\n    {\n        return FilesystemOperationFailed::OPERATION_READ;\n    }\n\n    public function reason(): string\n    {\n        return $this->reason;\n    }\n\n    public function location(): string\n    {\n        return $this->location;\n    }\n}\n"
  },
  {
    "path": "src/UnableToResolveFilesystemMount.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\n\nclass UnableToResolveFilesystemMount extends RuntimeException implements FilesystemException\n{\n    public static function becauseTheSeparatorIsMissing(string $path): UnableToResolveFilesystemMount\n    {\n        return new UnableToResolveFilesystemMount(\"Unable to resolve the filesystem mount because the path ($path) is missing a separator (://).\");\n    }\n\n    public static function becauseTheMountWasNotRegistered(string $mountIdentifier): UnableToResolveFilesystemMount\n    {\n        return new UnableToResolveFilesystemMount(\"Unable to resolve the filesystem mount because the mount ($mountIdentifier) was not registered.\");\n    }\n}\n"
  },
  {
    "path": "src/UnableToRetrieveMetadata.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\nuse Throwable;\n\nfinal class UnableToRetrieveMetadata extends RuntimeException implements FilesystemOperationFailed\n{\n    /**\n     * @var string\n     */\n    private $location;\n\n    /**\n     * @var string\n     */\n    private $metadataType;\n\n    /**\n     * @var string\n     */\n    private $reason;\n\n    public static function lastModified(string $location, string $reason = '', ?Throwable $previous = null): self\n    {\n        return static::create($location, FileAttributes::ATTRIBUTE_LAST_MODIFIED, $reason, $previous);\n    }\n\n    public static function visibility(string $location, string $reason = '', ?Throwable $previous = null): self\n    {\n        return static::create($location, FileAttributes::ATTRIBUTE_VISIBILITY, $reason, $previous);\n    }\n\n    public static function fileSize(string $location, string $reason = '', ?Throwable $previous = null): self\n    {\n        return static::create($location, FileAttributes::ATTRIBUTE_FILE_SIZE, $reason, $previous);\n    }\n\n    public static function mimeType(string $location, string $reason = '', ?Throwable $previous = null): self\n    {\n        return static::create($location, FileAttributes::ATTRIBUTE_MIME_TYPE, $reason, $previous);\n    }\n\n    public static function create(string $location, string $type, string $reason = '', ?Throwable $previous = null): self\n    {\n        $e = new static(\"Unable to retrieve the $type for file at location: $location. {$reason}\", 0, $previous);\n        $e->reason = $reason;\n        $e->location = $location;\n        $e->metadataType = $type;\n\n        return $e;\n    }\n\n    public function reason(): string\n    {\n        return $this->reason;\n    }\n\n    public function location(): string\n    {\n        return $this->location;\n    }\n\n    public function metadataType(): string\n    {\n        return $this->metadataType;\n    }\n\n    public function operation(): string\n    {\n        return FilesystemOperationFailed::OPERATION_RETRIEVE_METADATA;\n    }\n}\n"
  },
  {
    "path": "src/UnableToSetVisibility.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\n\nuse Throwable;\n\nuse function rtrim;\n\nfinal class UnableToSetVisibility extends RuntimeException implements FilesystemOperationFailed\n{\n    /**\n     * @var string\n     */\n    private $location;\n\n    /**\n     * @var string\n     */\n    private $reason;\n\n    public function reason(): string\n    {\n        return $this->reason;\n    }\n\n    public static function atLocation(string $filename, string $extraMessage = '', ?Throwable $previous = null): self\n    {\n        $message = \"Unable to set visibility for file {$filename}. $extraMessage\";\n        $e = new static(rtrim($message), 0, $previous);\n        $e->reason = $extraMessage;\n        $e->location = $filename;\n\n        return $e;\n    }\n\n    public function operation(): string\n    {\n        return FilesystemOperationFailed::OPERATION_SET_VISIBILITY;\n    }\n\n    public function location(): string\n    {\n        return $this->location;\n    }\n}\n"
  },
  {
    "path": "src/UnableToWriteFile.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\nuse Throwable;\n\nfinal class UnableToWriteFile extends RuntimeException implements FilesystemOperationFailed\n{\n    /**\n     * @var string\n     */\n    private $location = '';\n\n    /**\n     * @var string\n     */\n    private $reason;\n\n    public static function atLocation(string $location, string $reason = '', ?Throwable $previous = null): UnableToWriteFile\n    {\n        $e = new static(rtrim(\"Unable to write file at location: {$location}. {$reason}\"), 0, $previous);\n        $e->location = $location;\n        $e->reason = $reason;\n\n        return $e;\n    }\n\n    public function operation(): string\n    {\n        return FilesystemOperationFailed::OPERATION_WRITE;\n    }\n\n    public function reason(): string\n    {\n        return $this->reason;\n    }\n\n    public function location(): string\n    {\n        return $this->location;\n    }\n}\n"
  },
  {
    "path": "src/UnixVisibility/PortableVisibilityConverter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\UnixVisibility;\n\nuse League\\Flysystem\\PortableVisibilityGuard;\nuse League\\Flysystem\\Visibility;\n\nclass PortableVisibilityConverter implements VisibilityConverter\n{\n    public function __construct(\n        private int $filePublic = 0644,\n        private int $filePrivate = 0600,\n        private int $directoryPublic = 0755,\n        private int $directoryPrivate = 0700,\n        private string $defaultForDirectories = Visibility::PRIVATE\n    ) {\n    }\n\n    public function forFile(string $visibility): int\n    {\n        PortableVisibilityGuard::guardAgainstInvalidInput($visibility);\n\n        return $visibility === Visibility::PUBLIC\n            ? $this->filePublic\n            : $this->filePrivate;\n    }\n\n    public function forDirectory(string $visibility): int\n    {\n        PortableVisibilityGuard::guardAgainstInvalidInput($visibility);\n\n        return $visibility === Visibility::PUBLIC\n            ? $this->directoryPublic\n            : $this->directoryPrivate;\n    }\n\n    public function inverseForFile(int $visibility): string\n    {\n        if ($visibility === $this->filePublic) {\n            return Visibility::PUBLIC;\n        } elseif ($visibility === $this->filePrivate) {\n            return Visibility::PRIVATE;\n        }\n\n        return Visibility::PUBLIC; // default\n    }\n\n    public function inverseForDirectory(int $visibility): string\n    {\n        if ($visibility === $this->directoryPublic) {\n            return Visibility::PUBLIC;\n        } elseif ($visibility === $this->directoryPrivate) {\n            return Visibility::PRIVATE;\n        }\n\n        return Visibility::PUBLIC; // default\n    }\n\n    public function defaultForDirectories(): int\n    {\n        return $this->defaultForDirectories === Visibility::PUBLIC ? $this->directoryPublic : $this->directoryPrivate;\n    }\n\n    /**\n     * @param array<mixed>  $permissionMap\n     */\n    public static function fromArray(array $permissionMap, string $defaultForDirectories = Visibility::PRIVATE): PortableVisibilityConverter\n    {\n        return new PortableVisibilityConverter(\n            $permissionMap['file']['public'] ?? 0644,\n            $permissionMap['file']['private'] ?? 0600,\n            $permissionMap['dir']['public'] ?? 0755,\n            $permissionMap['dir']['private'] ?? 0700,\n            $defaultForDirectories\n        );\n    }\n}\n"
  },
  {
    "path": "src/UnixVisibility/PortableVisibilityConverterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\UnixVisibility;\n\nuse League\\Flysystem\\InvalidVisibilityProvided;\nuse League\\Flysystem\\Visibility;\nuse PHPUnit\\Framework\\TestCase;\n\n/**\n * @group unix-visibility\n */\nclass PortableVisibilityConverterTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function determining_visibility_for_a_file(): void\n    {\n        $interpreter = new PortableVisibilityConverter();\n        $this->assertEquals(0644, $interpreter->forFile(Visibility::PUBLIC));\n        $this->assertEquals(0600, $interpreter->forFile(Visibility::PRIVATE));\n    }\n\n    /**\n     * @test\n     */\n    public function determining_an_incorrect_visibility_for_a_file(): void\n    {\n        $this->expectException(InvalidVisibilityProvided::class);\n        $interpreter = new PortableVisibilityConverter();\n        $interpreter->forFile('incorrect');\n    }\n\n    /**\n     * @test\n     */\n    public function determining_visibility_for_a_directory(): void\n    {\n        $interpreter = new PortableVisibilityConverter();\n        $this->assertEquals(0755, $interpreter->forDirectory(Visibility::PUBLIC));\n        $this->assertEquals(0700, $interpreter->forDirectory(Visibility::PRIVATE));\n    }\n\n    /**\n     * @test\n     */\n    public function determining_an_incorrect_visibility_for_a_directory(): void\n    {\n        $this->expectException(InvalidVisibilityProvided::class);\n        $interpreter = new PortableVisibilityConverter();\n        $interpreter->forDirectory('incorrect');\n    }\n\n    /**\n     * @test\n     */\n    public function inversing_for_a_file(): void\n    {\n        $interpreter = new PortableVisibilityConverter();\n        $this->assertEquals(Visibility::PUBLIC, $interpreter->inverseForFile(0644));\n        $this->assertEquals(Visibility::PRIVATE, $interpreter->inverseForFile(0600));\n        $this->assertEquals(Visibility::PUBLIC, $interpreter->inverseForFile(0404));\n    }\n\n    /**\n     * @test\n     */\n    public function inversing_for_a_directory(): void\n    {\n        $interpreter = new PortableVisibilityConverter();\n        $this->assertEquals(Visibility::PUBLIC, $interpreter->inverseForDirectory(0755));\n        $this->assertEquals(Visibility::PRIVATE, $interpreter->inverseForDirectory(0700));\n        $this->assertEquals(Visibility::PUBLIC, $interpreter->inverseForDirectory(0404));\n    }\n\n    /**\n     * @test\n     */\n    public function determining_default_for_directories(): void\n    {\n        $interpreter = new PortableVisibilityConverter();\n        $this->assertEquals(0700, $interpreter->defaultForDirectories());\n\n        $interpreter = new PortableVisibilityConverter(0644, 0600, 0755, 0700, Visibility::PUBLIC);\n        $this->assertEquals(0755, $interpreter->defaultForDirectories());\n    }\n\n    /**\n     * @test\n     */\n    public function creating_from_array(): void\n    {\n        $interpreter = PortableVisibilityConverter::fromArray([\n            'file' => [\n                'public' => 0640,\n                'private' => 0604,\n            ],\n            'dir' => [\n                'public' => 0740,\n                'private' => 7604,\n            ],\n        ]);\n\n        $this->assertEquals(0640, $interpreter->forFile(Visibility::PUBLIC));\n        $this->assertEquals(0604, $interpreter->forFile(Visibility::PRIVATE));\n\n        $this->assertEquals(0740, $interpreter->forDirectory(Visibility::PUBLIC));\n        $this->assertEquals(7604, $interpreter->forDirectory(Visibility::PRIVATE));\n    }\n}\n"
  },
  {
    "path": "src/UnixVisibility/VisibilityConverter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\UnixVisibility;\n\ninterface VisibilityConverter\n{\n    public function forFile(string $visibility): int;\n    public function forDirectory(string $visibility): int;\n    public function inverseForFile(int $visibility): string;\n    public function inverseForDirectory(int $visibility): string;\n    public function defaultForDirectories(): int;\n}\n"
  },
  {
    "path": "src/UnreadableFileEncountered.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse RuntimeException;\n\nfinal class UnreadableFileEncountered extends RuntimeException implements FilesystemException\n{\n    /**\n     * @var string\n     */\n    private $location;\n\n    public function location(): string\n    {\n        return $this->location;\n    }\n\n    public static function atLocation(string $location): UnreadableFileEncountered\n    {\n        $e = new static(\"Unreadable file encountered at location {$location}.\");\n        $e->location = $location;\n\n        return $e;\n    }\n}\n"
  },
  {
    "path": "src/UrlGeneration/ChainedPublicUrlGenerator.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\UrlGeneration;\n\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\UnableToGeneratePublicUrl;\n\nfinal class ChainedPublicUrlGenerator implements PublicUrlGenerator\n{\n    /**\n     * @param PublicUrlGenerator[] $generators\n     */\n    public function __construct(private iterable $generators)\n    {\n    }\n\n    public function publicUrl(string $path, Config $config): string\n    {\n        foreach ($this->generators as $generator) {\n            try {\n                return $generator->publicUrl($path, $config);\n            } catch (UnableToGeneratePublicUrl) {\n            }\n        }\n\n        throw new UnableToGeneratePublicUrl('No supported public url generator found.', $path);\n    }\n}\n"
  },
  {
    "path": "src/UrlGeneration/ChainedPublicUrlGeneratorTest.php",
    "content": "<?php\n\nnamespace League\\Flysystem\\UrlGeneration;\n\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\UnableToGeneratePublicUrl;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class ChainedPublicUrlGeneratorTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function can_generate_url_for_supported_generator(): void\n    {\n        $generator = new ChainedPublicUrlGenerator([\n            new class() implements PublicUrlGenerator {\n                public function publicUrl(string $path, Config $config): string\n                {\n                    throw new UnableToGeneratePublicUrl('not supported', $path);\n                }\n            },\n            new PrefixPublicUrlGenerator('/prefix'),\n        ]);\n\n        $this->assertSame('/prefix/some/path', $generator->publicUrl('some/path', new Config()));\n    }\n\n    /**\n     * @test\n     */\n    public function no_supported_generator_found_throws_exception(): void\n    {\n        $generator = new ChainedPublicUrlGenerator([\n            new class() implements PublicUrlGenerator {\n                public function publicUrl(string $path, Config $config): string\n                {\n                    throw new UnableToGeneratePublicUrl('not supported', $path);\n                }\n            },\n        ]);\n\n        $this->expectException(UnableToGeneratePublicUrl::class);\n        $this->expectExceptionMessage('Unable to generate public url for some/path: No supported public url generator found.');\n\n        $generator->publicUrl('some/path', new Config());\n    }\n}\n"
  },
  {
    "path": "src/UrlGeneration/PrefixPublicUrlGenerator.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\UrlGeneration;\n\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\PathPrefixer;\n\nclass PrefixPublicUrlGenerator implements PublicUrlGenerator\n{\n    private PathPrefixer $prefixer;\n\n    public function __construct(string $urlPrefix)\n    {\n        $this->prefixer = new PathPrefixer($urlPrefix, '/');\n    }\n\n    public function publicUrl(string $path, Config $config): string\n    {\n        return $this->prefixer->prefixPath($path);\n    }\n}\n"
  },
  {
    "path": "src/UrlGeneration/PublicUrlGenerator.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\UrlGeneration;\n\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\UnableToGeneratePublicUrl;\n\ninterface PublicUrlGenerator\n{\n    /**\n     * @throws UnableToGeneratePublicUrl\n     */\n    public function publicUrl(string $path, Config $config): string;\n}\n"
  },
  {
    "path": "src/UrlGeneration/ShardedPrefixPublicUrlGenerator.php",
    "content": "<?php\n\nnamespace League\\Flysystem\\UrlGeneration;\n\nuse InvalidArgumentException;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\PathPrefixer;\n\nuse function array_map;\nuse function count;\nuse function crc32;\n\nfinal class ShardedPrefixPublicUrlGenerator implements PublicUrlGenerator\n{\n    /** @var PathPrefixer[] */\n    private array $prefixes;\n    private int $count;\n\n    /**\n     * @param string[] $prefixes\n     */\n    public function __construct(array $prefixes)\n    {\n        $this->count = count($prefixes);\n\n        if ($this->count === 0) {\n            throw new InvalidArgumentException('At least one prefix is required.');\n        }\n\n        $this->prefixes = array_map(static fn (string $prefix) => new PathPrefixer($prefix, '/'), $prefixes);\n    }\n\n    public function publicUrl(string $path, Config $config): string\n    {\n        $index = abs(crc32($path)) % $this->count;\n\n        return $this->prefixes[$index]->prefixPath($path);\n    }\n}\n"
  },
  {
    "path": "src/UrlGeneration/TemporaryUrlGenerator.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\UrlGeneration;\n\nuse DateTimeInterface;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\UnableToGenerateTemporaryUrl;\n\ninterface TemporaryUrlGenerator\n{\n    /**\n     * @throws UnableToGenerateTemporaryUrl\n     */\n    public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string;\n}\n"
  },
  {
    "path": "src/Visibility.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nfinal class Visibility\n{\n    public const PUBLIC = 'public';\n    public const PRIVATE = 'private';\n}\n"
  },
  {
    "path": "src/WebDAV/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n**/*Test.php export-ignore\n**/*TestCase.php export-ignore\n**/*Stub.php export-ignore\nresources export-ignore\nREADME.md export-ignore\n"
  },
  {
    "path": "src/WebDAV/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/WebDAV/ByteMarkWebDAVServerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\WebDAV;\n\nuse League\\Flysystem\\FilesystemAdapter;\n\nclass ByteMarkWebDAVServerTest extends WebDAVAdapterTestCase\n{\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        if (($_ENV['TEST_WEBDAV'] ?? '') !== 'YES') {\n            self::markTestSkipped('Library regression');\n        }\n        $client = new UrlPrefixingClientStub(['baseUri' => 'http://localhost:4080/', 'userName' => 'alice', 'password' => 'secret1234']);\n\n        return new WebDAVAdapter($client, manualCopy: true, manualMove: true);\n    }\n}\n"
  },
  {
    "path": "src/WebDAV/LICENSE",
    "content": "Copyright (c) 2013-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/WebDAV/README.md",
    "content": "## Sub-split of Flysystem for WebDAV using sabre/dav.\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n\n```bash\ncomposer require league/flysystem-webdav\n```\n\nView the [documentation](https://flysystem.thephpleague.com/docs/adapter/webdav).\n"
  },
  {
    "path": "src/WebDAV/SabreServerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\WebDAV;\n\nuse League\\Flysystem\\FilesystemAdapter;\nuse Sabre\\DAV\\Client;\n\nclass SabreServerTest extends WebDAVAdapterTestCase\n{\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        $client = new Client(['baseUri' => 'http://localhost:4040/']);\n\n        return new WebDAVAdapter($client, 'directory/prefix');\n    }\n}\n"
  },
  {
    "path": "src/WebDAV/UrlPrefixingClientStub.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\WebDAV;\n\nuse Sabre\\DAV\\Client;\n\nclass UrlPrefixingClientStub extends Client\n{\n    /**\n     * @param string $url\n     */\n    public function propFind($url, array $properties, $depth = 0): array\n    {\n        $response = parent::propFind($url, $properties, $depth);\n\n        if ($depth === 0) {\n            return $response;\n        }\n\n        $formatted = [];\n\n        foreach ($response as $path => $object) {\n            $formatted['https://domain.tld/' . ltrim($path, '/')] = $object;\n        }\n\n        return $formatted;\n    }\n}\n"
  },
  {
    "path": "src/WebDAV/WebDAVAdapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\WebDAV;\n\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\UnableToCheckDirectoryExistence;\nuse League\\Flysystem\\UnableToCheckFileExistence;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UrlGeneration\\PublicUrlGenerator;\nuse RuntimeException;\nuse Sabre\\DAV\\Client;\nuse Sabre\\DAV\\Xml\\Property\\ResourceType;\nuse Sabre\\HTTP\\ClientHttpException;\nuse Sabre\\HTTP\\Request;\nuse Throwable;\n\nuse function array_key_exists;\nuse function array_shift;\nuse function dirname;\nuse function explode;\nuse function fclose;\nuse function implode;\nuse function parse_url;\nuse function rawurldecode;\n\nclass WebDAVAdapter implements FilesystemAdapter, PublicUrlGenerator\n{\n    public const ON_VISIBILITY_THROW_ERROR = 'throw';\n    public const ON_VISIBILITY_IGNORE = 'ignore';\n    public const FIND_PROPERTIES = [\n        '{DAV:}displayname',\n        '{DAV:}getcontentlength',\n        '{DAV:}getcontenttype',\n        '{DAV:}getlastmodified',\n        '{DAV:}iscollection',\n        '{DAV:}resourcetype',\n    ];\n\n    private PathPrefixer $prefixer;\n\n    public function __construct(\n        private Client $client,\n        string $prefix = '',\n        private string $visibilityHandling = self::ON_VISIBILITY_THROW_ERROR,\n        private bool $manualCopy = false,\n        private bool $manualMove = false,\n    ) {\n        $this->prefixer = new PathPrefixer($prefix);\n    }\n\n    public function fileExists(string $path): bool\n    {\n        $location = $this->encodePath($this->prefixer->prefixPath($path));\n\n        try {\n            $properties = $this->client->propFind($location, ['{DAV:}resourcetype', '{DAV:}iscollection']);\n\n            return ! $this->propsIsDirectory($properties);\n        } catch (Throwable $exception) {\n            if ($exception instanceof ClientHttpException && $exception->getHttpStatus() === 404) {\n                return false;\n            }\n\n            throw UnableToCheckFileExistence::forLocation($path, $exception);\n        }\n    }\n\n    protected function encodePath(string $path): string\n    {\n        $parts = explode('/', $path);\n\n        foreach ($parts as $i => $part) {\n            $parts[$i] = rawurlencode($part);\n        }\n\n        return implode('/', $parts);\n    }\n\n    public function directoryExists(string $path): bool\n    {\n        $location = $this->encodePath($this->prefixer->prefixPath($path));\n\n        try {\n            $properties = $this->client->propFind($location, ['{DAV:}resourcetype', '{DAV:}iscollection']);\n\n            return $this->propsIsDirectory($properties);\n        } catch (Throwable $exception) {\n            if ($exception instanceof ClientHttpException && $exception->getHttpStatus() === 404) {\n                return false;\n            }\n\n            throw UnableToCheckDirectoryExistence::forLocation($path, $exception);\n        }\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        $this->upload($path, $contents);\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        $this->upload($path, $contents);\n    }\n\n    /**\n     * @param resource|string $contents\n     */\n    private function upload(string $path, mixed $contents): void\n    {\n        $this->createParentDirFor($path);\n        $location = $this->encodePath($this->prefixer->prefixPath($path));\n\n        try {\n            $response = $this->client->request('PUT', $location, $contents);\n            $statusCode = $response['statusCode'];\n\n            if ($statusCode < 200 || $statusCode >= 300) {\n                throw new RuntimeException('Unexpected status code received: ' . $statusCode);\n            }\n        } catch (Throwable $exception) {\n            throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function read(string $path): string\n    {\n        $location = $this->encodePath($this->prefixer->prefixPath($path));\n\n        try {\n            $response = $this->client->request('GET', $location);\n\n            if ($response['statusCode'] !== 200) {\n                throw new RuntimeException('Unexpected response code for GET: ' . $response['statusCode']);\n            }\n\n            return $response['body'];\n        } catch (Throwable $exception) {\n            throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function readStream(string $path)\n    {\n        $location = $this->encodePath($this->prefixer->prefixPath($path));\n\n        try {\n            $url = $this->client->getAbsoluteUrl($location);\n            $request = new Request('GET', $url);\n            $response = $this->client->send($request);\n            $status = $response->getStatus();\n\n            if ($status !== 200) {\n                throw new RuntimeException('Unexpected response code for GET: ' . $status);\n            }\n\n            return $response->getBodyAsStream();\n        } catch (Throwable $exception) {\n            throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function delete(string $path): void\n    {\n        $location = $this->encodePath($this->prefixer->prefixPath($path));\n\n        try {\n            $response = $this->client->request('DELETE', $location);\n            $statusCode = $response['statusCode'];\n\n            if ($statusCode !== 404 && ($statusCode < 200 || $statusCode >= 300)) {\n                throw new RuntimeException('Unexpected status code received while deleting file: ' . $statusCode);\n            }\n        } catch (Throwable $exception) {\n            if ( ! ($exception instanceof ClientHttpException && $exception->getCode() === 404)) {\n                throw UnableToDeleteFile::atLocation($path, $exception->getMessage(), $exception);\n            }\n        }\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        $location = $this->encodePath($this->prefixer->prefixDirectoryPath($path));\n\n        try {\n            $statusCode = $this->client->request('DELETE', $location)['statusCode'];\n\n            if ($statusCode !== 404 && ($statusCode < 200 || $statusCode >= 300)) {\n                throw new RuntimeException('Unexpected status code received while deleting file: ' . $statusCode);\n            }\n        } catch (Throwable $exception) {\n            if ( ! ($exception instanceof ClientHttpException && $exception->getCode() === 404)) {\n                throw UnableToDeleteDirectory::atLocation($path, $exception->getMessage(), $exception);\n            }\n        }\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        $parts = explode('/', $this->prefixer->prefixDirectoryPath($path));\n        $directoryParts = [];\n\n        foreach ($parts as $directory) {\n            if ($directory === '.' || $directory === '') {\n                return;\n            }\n\n            $directoryParts[] = $directory;\n            $directoryPath = implode('/', $directoryParts);\n            $location = $this->encodePath($directoryPath) . '/';\n\n            if ($this->directoryExists($this->prefixer->stripDirectoryPrefix($directoryPath))) {\n                continue;\n            }\n\n            try {\n                $response = $this->client->request('MKCOL', $location);\n            } catch (Throwable $exception) {\n                throw UnableToCreateDirectory::dueToFailure($path, $exception);\n            }\n\n            if ($response['statusCode'] === 405) {\n                continue;\n            }\n\n            if ($response['statusCode'] !== 201) {\n                throw UnableToCreateDirectory::atLocation($path, 'Failed to create directory at: ' . $location);\n            }\n        }\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        if ($this->visibilityHandling === self::ON_VISIBILITY_THROW_ERROR) {\n            throw UnableToSetVisibility::atLocation($path, 'WebDAV does not support this operation.');\n        }\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        throw UnableToRetrieveMetadata::visibility($path, 'WebDAV does not support this operation.');\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        $mimeType = (string) $this->propFind($path, 'mime_type', '{DAV:}getcontenttype');\n\n        return new FileAttributes($path, mimeType: $mimeType);\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        $lastModified = $this->propFind($path, 'last_modified', '{DAV:}getlastmodified');\n\n        return new FileAttributes($path, lastModified: strtotime($lastModified));\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        $fileSize = (int) $this->propFind($path, 'file_size', '{DAV:}getcontentlength');\n\n        return new FileAttributes($path, fileSize: $fileSize);\n    }\n\n    public function listContents(string $path, bool $deep): iterable\n    {\n        $location = $this->encodePath($this->prefixer->prefixDirectoryPath($path));\n        $response = $this->client->propFind($location, self::FIND_PROPERTIES, 1);\n\n        // This is the directory itself, the files are subsequent entries.\n        array_shift($response);\n\n        foreach ($response as $path => $object) {\n            $path = (string) parse_url(rawurldecode($path), PHP_URL_PATH);\n            $path = $this->prefixer->stripPrefix($path);\n            $object = $this->normalizeObject($object);\n\n            if ($this->propsIsDirectory($object)) {\n                yield new DirectoryAttributes($path, lastModified: $object['last_modified'] ?? null);\n\n                if ( ! $deep) {\n                    continue;\n                }\n\n                foreach ($this->listContents($path, true) as $child) {\n                    yield $child;\n                }\n            } else {\n                yield new FileAttributes(\n                    $path,\n                    fileSize:     $object['file_size'] ?? null,\n                    lastModified: $object['last_modified'] ?? null,\n                    mimeType:     $object['mime_type'] ?? null,\n                );\n            }\n        }\n    }\n\n    private function normalizeObject(array $object): array\n    {\n        $mapping = [\n            '{DAV:}getcontentlength' => 'file_size',\n            '{DAV:}getcontenttype' => 'mime_type',\n            'content-length' => 'file_size',\n            'content-type' => 'mime_type',\n        ];\n\n        foreach ($mapping as $from => $to) {\n            if (array_key_exists($from, $object)) {\n                $object[$to] = $object[$from];\n            }\n        }\n\n        array_key_exists('file_size', $object) && $object['file_size'] = (int) $object['file_size'];\n\n        if (array_key_exists('{DAV:}getlastmodified', $object)) {\n            $object['last_modified'] = strtotime($object['{DAV:}getlastmodified']);\n        }\n\n        return $object;\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        if ($source === $destination) {\n            return;\n        }\n\n        if ($this->manualMove) {\n            $this->manualMove($source, $destination);\n\n            return;\n        }\n\n        $this->createParentDirFor($destination);\n        $location = $this->encodePath($this->prefixer->prefixPath($source));\n        $newLocation = $this->encodePath($this->prefixer->prefixPath($destination));\n\n        try {\n            $response = $this->client->request('MOVE', $location, null, [\n                'Destination' => $this->client->getAbsoluteUrl($newLocation),\n            ]);\n\n            if ($response['statusCode'] < 200 || $response['statusCode'] >= 300) {\n                throw new RuntimeException('MOVE command returned unexpected status code: ' . $response['statusCode'] . \"\\n{$response['body']}\");\n            }\n        } catch (Throwable $e) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $e);\n        }\n    }\n\n    private function manualMove(string $source, string $destination): void\n    {\n        try {\n            $handle = $this->readStream($source);\n            $this->writeStream($destination, $handle, new Config());\n            @fclose($handle);\n            $this->delete($source);\n        } catch (Throwable $exception) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        if ($source === $destination) {\n            return;\n        }\n\n        if ($this->manualCopy) {\n            $this->manualCopy($source, $destination);\n\n            return;\n        }\n\n        $this->createParentDirFor($destination);\n        $location = $this->encodePath($this->prefixer->prefixPath($source));\n        $newLocation = $this->encodePath($this->prefixer->prefixPath($destination));\n\n        try {\n            $response = $this->client->request('COPY', $location, null, [\n                'Destination' => $this->client->getAbsoluteUrl($newLocation),\n            ]);\n\n            if ($response['statusCode'] < 200 || $response['statusCode'] >= 300) {\n                throw new RuntimeException('COPY command returned unexpected status code: ' . $response['statusCode']);\n            }\n        } catch (Throwable $e) {\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $e);\n        }\n    }\n\n    private function manualCopy(string $source, string $destination): void\n    {\n        try {\n            $handle = $this->readStream($source);\n            $this->writeStream($destination, $handle, new Config());\n            @fclose($handle);\n        } catch (Throwable $exception) {\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    private function propsIsDirectory(array $properties): bool\n    {\n        if (isset($properties['{DAV:}resourcetype'])) {\n            /** @var ResourceType $resourceType */\n            $resourceType = $properties['{DAV:}resourcetype'];\n\n            return $resourceType->is('{DAV:}collection');\n        }\n\n        return isset($properties['{DAV:}iscollection']) && $properties['{DAV:}iscollection'] === '1';\n    }\n\n    private function createParentDirFor(string $path): void\n    {\n        $dirname = dirname($path);\n\n        if ($this->directoryExists($dirname)) {\n            return;\n        }\n\n        $this->createDirectory($dirname, new Config());\n    }\n\n    private function propFind(string $path, string $section, string $property): mixed\n    {\n        $location = $this->encodePath($this->prefixer->prefixPath($path));\n\n        try {\n            $result = $this->client->propFind($location, [$property]);\n\n            if ( ! array_key_exists($property, $result)) {\n                throw new RuntimeException('Invalid response, missing key: ' . $property);\n            }\n\n            return $result[$property];\n        } catch (Throwable $exception) {\n            throw UnableToRetrieveMetadata::create($path, $section, $exception->getMessage(), $exception);\n        }\n    }\n\n    public function publicUrl(string $path, Config $config): string\n    {\n        return $this->client->getAbsoluteUrl($this->encodePath($this->prefixer->prefixPath($path)));\n    }\n}\n"
  },
  {
    "path": "src/WebDAV/WebDAVAdapterTestCase.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\WebDAV;\n\nuse League\\Flysystem\\AdapterTestUtilities\\FilesystemAdapterTestCase;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\Visibility;\nuse Sabre\\DAV\\Client;\n\nabstract class WebDAVAdapterTestCase extends FilesystemAdapterTestCase\n{\n    /**\n     * @test\n     */\n    public function setting_visibility(): void\n    {\n        $adapter = $this->adapter();\n        $this->givenWeHaveAnExistingFile('some/file.txt');\n\n        $this->expectException(UnableToSetVisibility::class);\n\n        $adapter->setVisibility('some/file.txt', Visibility::PRIVATE);\n    }\n\n    /**\n     * @test\n     */\n    public function overwriting_a_file(): void\n    {\n        $this->runScenario(function () {\n            $this->givenWeHaveAnExistingFile('path.txt', 'contents');\n            $adapter = $this->adapter();\n\n            $adapter->write('path.txt', 'new contents', new Config());\n\n            $contents = $adapter->read('path.txt');\n            $this->assertEquals('new contents', $contents);\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function creating_a_directory_with_leading_and_trailing_slashes(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->createDirectory('/some/directory/', new Config());\n\n            self::assertTrue($adapter->directoryExists('/some/directory/'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be copied',\n                new Config()\n            );\n\n            $adapter->copy('source.txt', 'destination.txt', new Config());\n\n            $this->assertTrue($adapter->fileExists('source.txt'));\n            $this->assertTrue($adapter->fileExists('destination.txt'));\n            $this->assertEquals('contents to be copied', $adapter->read('destination.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function copying_a_file_again(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be copied',\n                new Config()\n            );\n\n            $adapter->copy('source.txt', 'destination.txt', new Config());\n\n            $this->assertTrue($adapter->fileExists('source.txt'));\n            $this->assertTrue($adapter->fileExists('destination.txt'));\n            $this->assertEquals('contents to be copied', $adapter->read('destination.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function moving_a_file(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be copied',\n                new Config()\n            );\n            $adapter->move('source.txt', 'destination.txt', new Config());\n            $this->assertFalse(\n                $adapter->fileExists('source.txt'),\n                'After moving a file should no longer exist in the original location.'\n            );\n            $this->assertTrue(\n                $adapter->fileExists('destination.txt'),\n                'After moving, a file should be present at the new location.'\n            );\n            $this->assertEquals('contents to be copied', $adapter->read('destination.txt'));\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function moving_a_file_that_does_not_exist(): void\n    {\n        $this->expectException(UnableToMoveFile::class);\n\n        $this->runScenario(function () {\n            $this->adapter()->move('source.txt', 'destination.txt', new Config());\n        });\n    }\n\n    /**\n     * @test\n     */\n    public function part_of_prefix_already_exists(): void\n    {\n        $this->runScenario(function () {\n            $config = new Config();\n\n            $adapter1 = new WebDAVAdapter(\n                new Client(['baseUri' => 'http://localhost:4040/']),\n                'directory1/prefix1',\n            );\n            $adapter1->createDirectory('folder1', $config);\n            self::assertTrue($adapter1->directoryExists('/folder1'));\n\n            $adapter2 = new WebDAVAdapter(\n                new Client(['baseUri' => 'http://localhost:4040/']),\n                'directory1/prefix2',\n            );\n            $adapter2->createDirectory('folder2', $config);\n            self::assertTrue($adapter2->directoryExists('/folder2'));\n        });\n    }\n}\n"
  },
  {
    "path": "src/WebDAV/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-webdav\",\n    \"description\": \"WebDAV filesystem adapter for Flysystem.\",\n    \"keywords\": [\"flysystem\", \"filesystem\", \"webdav\", \"files\", \"file\"],\n    \"autoload\": {\n        \"psr-4\": {\n                \"League\\\\Flysystem\\\\WebDAV\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"league/flysystem\": \"^3.6.0\",\n        \"sabre/dav\": \"^4.6.0\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ]\n}\n"
  },
  {
    "path": "src/WebDAV/resources/.gitignore",
    "content": "data\nindex.php\n"
  },
  {
    "path": "src/WebDAV/resources/server.php",
    "content": "<?php\n\nuse Sabre\\DAV\\FS\\Directory;\nuse Sabre\\DAV\\Server;\n\ninclude __DIR__ . '/../../../vendor/autoload.php';\n\nerror_reporting(E_ALL ^ E_DEPRECATED);\n\n$rootPath = __DIR__ . '/data';\n\nif ( ! is_dir($rootPath)) {\n    mkdir($rootPath);\n}\n\n$rootDirectory = new Directory($rootPath);\n$server = new Server($rootDirectory);\n$server->addPlugin(new Sabre\\DAV\\Browser\\Plugin());\n\nif (strpos($_SERVER['REQUEST_URI'], 'unknown-mime-type.md5') === false) {\n    $guesser = new Sabre\\DAV\\Browser\\GuessContentType();\n    $guesser->extensionMap['svg'] = 'image/svg+xml';\n    $server->addPlugin($guesser);\n}\n\n$server->start();\n"
  },
  {
    "path": "src/WhitespacePathNormalizer.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nclass WhitespacePathNormalizer implements PathNormalizer\n{\n    public function normalizePath(string $path): string\n    {\n        $path = str_replace('\\\\', '/', $path);\n        $this->rejectFunkyWhiteSpace($path);\n\n        return $this->normalizeRelativePath($path);\n    }\n\n    private function rejectFunkyWhiteSpace(string $path): void\n    {\n        if (preg_match('#\\p{C}+#u', $path)) {\n            throw CorruptedPathDetected::forPath($path);\n        }\n    }\n\n    private function normalizeRelativePath(string $path): string\n    {\n        $parts = [];\n\n        foreach (explode('/', $path) as $part) {\n            switch ($part) {\n                case '':\n                case '.':\n                    break;\n\n                case '..':\n                    if (empty($parts)) {\n                        throw PathTraversalDetected::forPath($path);\n                    }\n                    array_pop($parts);\n                    break;\n\n                default:\n                    $parts[] = $part;\n                    break;\n            }\n        }\n\n        return implode('/', $parts);\n    }\n}\n"
  },
  {
    "path": "src/WhitespacePathNormalizerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem;\n\nuse PHPUnit\\Framework\\TestCase;\n\nclass WhitespacePathNormalizerTest extends TestCase\n{\n    /**\n     * @var WhitespacePathNormalizer\n     */\n    private $normalizer;\n\n    protected function setUp(): void\n    {\n        $this->normalizer = new WhitespacePathNormalizer();\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider  pathProvider\n     */\n    public function path_normalizing(string $input, string $expected): void\n    {\n        $result = $this->normalizer->normalizePath($input);\n        $double = $this->normalizer->normalizePath($this->normalizer->normalizePath($input));\n        $this->assertEquals($expected, $result);\n        $this->assertEquals($expected, $double);\n    }\n\n    /**\n     * @return array<array<string>>\n     */\n    public static function pathProvider(): array\n    {\n        return [\n            ['.', ''],\n            ['/path/to/dir/.', 'path/to/dir'],\n            ['/dirname/', 'dirname'],\n            ['dirname/..', ''],\n            ['dirname/../', ''],\n            ['dirname./', 'dirname.'],\n            ['dirname/./', 'dirname'],\n            ['dirname/.', 'dirname'],\n            ['./dir/../././', ''],\n            ['/something/deep/../../dirname', 'dirname'],\n            ['00004869/files/other/10-75..stl', '00004869/files/other/10-75..stl'],\n            ['/dirname//subdir///subsubdir', 'dirname/subdir/subsubdir'],\n            ['\\dirname\\\\\\\\subdir\\\\\\\\\\\\subsubdir', 'dirname/subdir/subsubdir'],\n            ['\\\\\\\\some\\shared\\\\\\\\drive', 'some/shared/drive'],\n            ['C:\\dirname\\\\\\\\subdir\\\\\\\\\\\\subsubdir', 'C:/dirname/subdir/subsubdir'],\n            ['C:\\\\\\\\dirname\\subdir\\\\\\\\subsubdir', 'C:/dirname/subdir/subsubdir'],\n            ['example/path/..txt', 'example/path/..txt'],\n            ['\\\\example\\\\path.txt', 'example/path.txt'],\n            ['\\\\example\\\\..\\\\path.txt', 'path.txt'],\n        ];\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider invalidPathProvider\n     */\n    public function guarding_against_path_traversal(string $input): void\n    {\n        $this->expectException(PathTraversalDetected::class);\n        $this->normalizer->normalizePath($input);\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider dpFunkyWhitespacePaths\n     */\n    public function rejecting_funky_whitespace(string $path): void\n    {\n        self::expectException(CorruptedPathDetected::class);\n        $this->normalizer->normalizePath($path);\n    }\n\n    public static function dpFunkyWhitespacePaths(): iterable\n    {\n        return [[\"some\\0/path.txt\"], [\"s\\x09i.php\"]];\n    }\n\n    /**\n     * @return array<array<string>>\n     */\n    public static function invalidPathProvider(): array\n    {\n        return [\n            ['something/../../../hehe'],\n            ['/something/../../..'],\n            ['..'],\n            ['something\\\\..\\\\..'],\n            ['\\\\something\\\\..\\\\..\\\\dirname'],\n        ];\n    }\n}\n"
  },
  {
    "path": "src/ZipArchive/.gitattributes",
    "content": "* text=auto\n\n.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n**/*Test.php export-ignore\nREADME.md export-ignore\n"
  },
  {
    "path": "src/ZipArchive/.github/workflows/close-subsplit-prs.yaml",
    "content": "---\nname: Close sub-split PRs\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n  pull_request:\n    branches:\n      - 2.x\n      - 3.x\n  schedule:\n    - cron: '30 7 * * *'\n\njobs:\n  close_subsplit_prs:\n    runs-on: ubuntu-latest\n    name: Close sub-split PRs\n    steps:\n      - uses: frankdejonge/action-close-subsplit-pr@0.1.0\n        with:\n          close_pr: 'yes'\n          target_branch_match: '^(?!master).+$'\n          message: |\n            Hi :wave:,\n            \n            Thank you for contributing to Flysystem. Unfortunately, you've sent a PR to a read-only sub-split repository. \n\n            All pull requests should be directed towards: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/ZipArchive/FilesystemZipArchiveProvider.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\ZipArchive;\n\nuse ZipArchive;\n\nclass FilesystemZipArchiveProvider implements ZipArchiveProvider\n{\n    /**\n     * @var bool\n     */\n    private $parentDirectoryCreated = false;\n\n    public function __construct(private string $filename, private int $localDirectoryPermissions = 0700)\n    {\n    }\n\n    public function createZipArchive(): ZipArchive\n    {\n        if ($this->parentDirectoryCreated !== true) {\n            $this->parentDirectoryCreated = true;\n            $this->createParentDirectoryForZipArchive($this->filename);\n        }\n\n        return $this->openZipArchive();\n    }\n\n    private function createParentDirectoryForZipArchive(string $fullPath): void\n    {\n        $dirname = dirname($fullPath);\n\n        if (is_dir($dirname) || @mkdir($dirname, $this->localDirectoryPermissions, true)) {\n            return;\n        }\n\n        if ( ! is_dir($dirname)) {\n            throw UnableToCreateParentDirectory::atLocation($fullPath, error_get_last()['message'] ?? '');\n        }\n    }\n\n    private function openZipArchive(): ZipArchive\n    {\n        $archive = new ZipArchive();\n        $success = $archive->open($this->filename, ZipArchive::CREATE);\n\n        if ($success !== true) {\n            throw UnableToOpenZipArchive::atLocation($this->filename, $archive->getStatusString() ?: '');\n        }\n\n        return $archive;\n    }\n}\n"
  },
  {
    "path": "src/ZipArchive/LICENSE",
    "content": "Copyright (c) 2013-2026 Frank de Jonge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/ZipArchive/NoRootPrefixZipArchiveAdapterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\ZipArchive;\n\n/**\n * @group zip\n */\nfinal class NoRootPrefixZipArchiveAdapterTest extends ZipArchiveAdapterTestCase\n{\n    protected static function getRoot(): string\n    {\n        return '/';\n    }\n}\n"
  },
  {
    "path": "src/ZipArchive/PrefixedRootZipArchiveAdapterTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\ZipArchive;\n\n/**\n * @group zip\n */\nfinal class PrefixedRootZipArchiveAdapterTest extends ZipArchiveAdapterTestCase\n{\n    protected static function getRoot(): string\n    {\n        return '/prefixed-path';\n    }\n}\n"
  },
  {
    "path": "src/ZipArchive/README.md",
    "content": "## Sub-split for Flysystem's ZipArchive adapter\n\n> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/thephpleague/flysystem\n"
  },
  {
    "path": "src/ZipArchive/StubZipArchive.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\ZipArchive;\n\nuse ZipArchive;\n\nclass StubZipArchive extends ZipArchive\n{\n    private bool $failNextDirectoryCreation = false;\n    private bool $failNextWrite = false;\n    private bool $failNextDeleteName = false;\n    private bool $failWhenSettingVisibility = false;\n    private bool $failWhenDeletingAnIndex = false;\n\n    public function failNextDirectoryCreation(): void\n    {\n        $this->failNextDirectoryCreation = true;\n    }\n\n    /**\n     * @param string $dirname\n     * @param int    $flags\n     *\n     * @return bool\n     */\n    public function addEmptyDir($dirname, $flags = 0): bool\n    {\n        if ($this->failNextDirectoryCreation) {\n            $this->failNextDirectoryCreation = false;\n\n            return false;\n        }\n\n        return parent::addEmptyDir($dirname);\n    }\n\n    public function failNextWrite(): void\n    {\n        $this->failNextWrite = true;\n    }\n\n    /**\n     * @param string $localname\n     * @param string $contents\n     * @param int    $flags\n     *\n     * @return bool\n     */\n    public function addFromString($localname, $contents, $flags = 0): bool\n    {\n        if ($this->failNextWrite) {\n            $this->failNextWrite = false;\n\n            return false;\n        }\n\n        return parent::addFromString($localname, $contents);\n    }\n\n    public function failNextDeleteName(): void\n    {\n        $this->failNextDeleteName = true;\n    }\n\n    /**\n     * @return bool\n     */\n    public function deleteName($name): bool\n    {\n        if ($this->failNextDeleteName) {\n            $this->failNextDeleteName = false;\n\n            return false;\n        }\n\n        return parent::deleteName($name);\n    }\n\n    public function failWhenSettingVisibility(): void\n    {\n        $this->failWhenSettingVisibility = true;\n    }\n\n    public function setExternalAttributesName($name, $opsys, $attr, $flags = null): bool\n    {\n        if ($this->failWhenSettingVisibility) {\n            $this->failWhenSettingVisibility = false;\n\n            return false;\n        }\n\n        return parent::setExternalAttributesName($name, $opsys, $attr);\n    }\n\n    public function failWhenDeletingAnIndex(): void\n    {\n        $this->failWhenDeletingAnIndex = true;\n    }\n\n    public function deleteIndex($index): bool\n    {\n        if ($this->failWhenDeletingAnIndex) {\n            $this->failWhenDeletingAnIndex = false;\n\n            return false;\n        }\n\n        return parent::deleteIndex($index);\n    }\n}\n"
  },
  {
    "path": "src/ZipArchive/StubZipArchiveProvider.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\ZipArchive;\n\nuse ZipArchive;\n\nclass StubZipArchiveProvider implements ZipArchiveProvider\n{\n    private FilesystemZipArchiveProvider $provider;\n\n    /**\n     * @var StubZipArchive\n     */\n    private $archive;\n\n    public function __construct(private string $filename, int $localDirectoryPermissions = 0700)\n    {\n        $this->provider = new FilesystemZipArchiveProvider($filename, $localDirectoryPermissions);\n    }\n\n    public function createZipArchive(): ZipArchive\n    {\n        if ( ! $this->archive instanceof StubZipArchive) {\n            $zipArchive = $this->provider->createZipArchive();\n            $zipArchive->close();\n            unset($zipArchive);\n            $this->archive = new StubZipArchive();\n        }\n\n        $this->archive->open($this->filename, ZipArchive::CREATE);\n\n        return $this->archive;\n    }\n\n    public function stubbedZipArchive(): StubZipArchive\n    {\n        $this->createZipArchive();\n\n        return $this->archive;\n    }\n}\n"
  },
  {
    "path": "src/ZipArchive/UnableToCreateParentDirectory.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\ZipArchive;\n\nuse RuntimeException;\n\nclass UnableToCreateParentDirectory extends RuntimeException implements ZipArchiveException\n{\n    public static function atLocation(string $location, string $reason = ''): UnableToCreateParentDirectory\n    {\n        return new UnableToCreateParentDirectory(\n            rtrim(\"Unable to create the parent directory ($location): $reason\", ' :')\n        );\n    }\n}\n"
  },
  {
    "path": "src/ZipArchive/UnableToOpenZipArchive.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\ZipArchive;\n\nuse RuntimeException;\n\nfinal class UnableToOpenZipArchive extends RuntimeException implements ZipArchiveException\n{\n    public static function atLocation(string $location, string $reason = ''): self\n    {\n        return new self(rtrim(sprintf(\n            'Unable to open file at location: %s. %s',\n            $location,\n            $reason\n        )));\n    }\n}\n"
  },
  {
    "path": "src/ZipArchive/ZipArchiveAdapter.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\ZipArchive;\n\nuse Generator;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\DirectoryAttributes;\nuse League\\Flysystem\\FileAttributes;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\PathPrefixer;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToReadFile;\nuse League\\Flysystem\\UnableToRetrieveMetadata;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\UnixVisibility\\PortableVisibilityConverter;\nuse League\\Flysystem\\UnixVisibility\\VisibilityConverter;\nuse League\\MimeTypeDetection\\FinfoMimeTypeDetector;\nuse League\\MimeTypeDetection\\MimeTypeDetector;\nuse Throwable;\nuse ZipArchive;\n\nuse function fclose;\nuse function fopen;\nuse function rewind;\nuse function stream_copy_to_stream;\n\nfinal class ZipArchiveAdapter implements FilesystemAdapter\n{\n    private PathPrefixer $pathPrefixer;\n    private MimeTypeDetector$mimeTypeDetector;\n    private VisibilityConverter $visibility;\n\n    public function __construct(\n        private ZipArchiveProvider $zipArchiveProvider,\n        string $root = '',\n        ?MimeTypeDetector $mimeTypeDetector = null,\n        ?VisibilityConverter $visibility = null,\n        private bool $detectMimeTypeUsingPath = false,\n    ) {\n        $this->pathPrefixer = new PathPrefixer(ltrim($root, '/'));\n        $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();\n        $this->visibility = $visibility ?? new PortableVisibilityConverter();\n    }\n\n    public function fileExists(string $path): bool\n    {\n        $archive = $this->zipArchiveProvider->createZipArchive();\n        $fileExists = $archive->locateName($this->pathPrefixer->prefixPath($path)) !== false;\n        $archive->close();\n\n        return $fileExists;\n    }\n\n    public function write(string $path, string $contents, Config $config): void\n    {\n        try {\n            $this->ensureParentDirectoryExists($path, $config);\n        } catch (Throwable $exception) {\n            throw UnableToWriteFile::atLocation($path, 'creating parent directory failed', $exception);\n        }\n\n        $archive = $this->zipArchiveProvider->createZipArchive();\n        $prefixedPath = $this->pathPrefixer->prefixPath($path);\n\n        if ( ! $archive->addFromString($prefixedPath, $contents)) {\n            throw UnableToWriteFile::atLocation($path, 'writing the file failed');\n        }\n\n        $archive->close();\n        $archive = $this->zipArchiveProvider->createZipArchive();\n\n        $visibility = $config->get(Config::OPTION_VISIBILITY);\n        $visibilityResult = $visibility === null\n            || $this->setVisibilityAttribute($prefixedPath, $visibility, $archive);\n        $archive->close();\n\n        if ($visibilityResult === false) {\n            throw UnableToWriteFile::atLocation($path, 'setting visibility failed');\n        }\n    }\n\n    public function writeStream(string $path, $contents, Config $config): void\n    {\n        $contents = stream_get_contents($contents);\n\n        if ($contents === false) {\n            throw UnableToWriteFile::atLocation($path, 'Could not get contents of given resource.');\n        }\n\n        $this->write($path, $contents, $config);\n    }\n\n    public function read(string $path): string\n    {\n        $archive = $this->zipArchiveProvider->createZipArchive();\n        $contents = $archive->getFromName($this->pathPrefixer->prefixPath($path));\n        $statusString = $archive->getStatusString();\n        $archive->close();\n\n        if ($contents === false) {\n            throw UnableToReadFile::fromLocation($path, $statusString);\n        }\n\n        return $contents;\n    }\n\n    public function readStream(string $path)\n    {\n        $archive = $this->zipArchiveProvider->createZipArchive();\n        $resource = $archive->getStream($this->pathPrefixer->prefixPath($path));\n\n        if ($resource === false) {\n            $status = $archive->getStatusString();\n            $archive->close();\n            throw UnableToReadFile::fromLocation($path, $status);\n        }\n\n        $stream = fopen('php://temp', 'w+b');\n        stream_copy_to_stream($resource, $stream);\n        rewind($stream);\n        fclose($resource);\n\n        return $stream;\n    }\n\n    public function delete(string $path): void\n    {\n        $prefixedPath = $this->pathPrefixer->prefixPath($path);\n        $zipArchive = $this->zipArchiveProvider->createZipArchive();\n        $success = $zipArchive->locateName($prefixedPath) === false || $zipArchive->deleteName($prefixedPath);\n        $statusString = $zipArchive->getStatusString();\n        $zipArchive->close();\n\n        if ( ! $success) {\n            throw UnableToDeleteFile::atLocation($path, $statusString);\n        }\n    }\n\n    public function deleteDirectory(string $path): void\n    {\n        $archive = $this->zipArchiveProvider->createZipArchive();\n        $prefixedPath = $this->pathPrefixer->prefixDirectoryPath($path);\n\n        for ($i = $archive->numFiles; $i > 0; $i--) {\n            if (($stats = $archive->statIndex($i)) === false) {\n                continue;\n            }\n\n            $itemPath = $stats['name'];\n\n            if ( ! str_starts_with($itemPath, $prefixedPath)) {\n                continue;\n            }\n\n            if ( ! $archive->deleteIndex($i)) {\n                $statusString = $archive->getStatusString();\n                $archive->close();\n                throw UnableToDeleteDirectory::atLocation($path, $statusString);\n            }\n        }\n\n        $archive->deleteName($prefixedPath);\n\n        $archive->close();\n    }\n\n    public function createDirectory(string $path, Config $config): void\n    {\n        try {\n            $this->ensureDirectoryExists($path, $config);\n        } catch (Throwable $exception) {\n            throw UnableToCreateDirectory::dueToFailure($path, $exception);\n        }\n    }\n\n    public function directoryExists(string $path): bool\n    {\n        $archive = $this->zipArchiveProvider->createZipArchive();\n        $location = $this->pathPrefixer->prefixDirectoryPath($path);\n\n        return $archive->statName($location) !== false;\n    }\n\n    public function setVisibility(string $path, string $visibility): void\n    {\n        $archive = $this->zipArchiveProvider->createZipArchive();\n        $location = $this->pathPrefixer->prefixPath($path);\n        $stats = $archive->statName($location) ?: $archive->statName($location . '/');\n\n        if ($stats === false) {\n            $statusString = $archive->getStatusString();\n            $archive->close();\n            throw UnableToSetVisibility::atLocation($path, $statusString);\n        }\n\n        if ( ! $this->setVisibilityAttribute($stats['name'], $visibility, $archive)) {\n            $statusString1 = $archive->getStatusString();\n            $archive->close();\n            throw UnableToSetVisibility::atLocation($path, $statusString1);\n        }\n\n        $archive->close();\n    }\n\n    public function visibility(string $path): FileAttributes\n    {\n        $opsys = null;\n        $attr = null;\n        $archive = $this->zipArchiveProvider->createZipArchive();\n        $archive->getExternalAttributesName(\n            $this->pathPrefixer->prefixPath($path),\n            $opsys,\n            $attr\n        );\n        $archive->close();\n\n        if ($opsys !== ZipArchive::OPSYS_UNIX || $attr === null) {\n            throw UnableToRetrieveMetadata::visibility($path);\n        }\n\n        return new FileAttributes(\n            $path,\n            null,\n            $this->visibility->inverseForFile($attr >> 16)\n        );\n    }\n\n    public function mimeType(string $path): FileAttributes\n    {\n        try {\n            $mimetype = $this->detectMimeTypeUsingPath\n                ? $this->mimeTypeDetector->detectMimeTypeFromPath($path)\n                : $this->mimeTypeDetector->detectMimeType($path, $this->read($path));\n        } catch (Throwable $exception) {\n            throw UnableToRetrieveMetadata::mimeType($path, $exception->getMessage(), $exception);\n        }\n\n        if ($mimetype === null) {\n            throw UnableToRetrieveMetadata::mimeType($path, 'Unknown.');\n        }\n\n        return new FileAttributes($path, null, null, null, $mimetype);\n    }\n\n    public function lastModified(string $path): FileAttributes\n    {\n        $zipArchive = $this->zipArchiveProvider->createZipArchive();\n        $stats = $zipArchive->statName($this->pathPrefixer->prefixPath($path));\n        $statusString = $zipArchive->getStatusString();\n        $zipArchive->close();\n\n        if ($stats === false) {\n            throw UnableToRetrieveMetadata::lastModified($path, $statusString);\n        }\n\n        return new FileAttributes($path, null, null, $stats['mtime']);\n    }\n\n    public function fileSize(string $path): FileAttributes\n    {\n        $archive = $this->zipArchiveProvider->createZipArchive();\n        $stats = $archive->statName($this->pathPrefixer->prefixPath($path));\n        $statusString = $archive->getStatusString();\n        $archive->close();\n\n        if ($stats === false) {\n            throw UnableToRetrieveMetadata::fileSize($path, $statusString);\n        }\n\n        if ($this->isDirectoryPath($stats['name'])) {\n            throw UnableToRetrieveMetadata::fileSize($path, 'It\\'s a directory.');\n        }\n\n        return new FileAttributes($path, $stats['size'], null, null);\n    }\n\n    public function listContents(string $path, bool $deep): iterable\n    {\n        $archive = $this->zipArchiveProvider->createZipArchive();\n        $location = $this->pathPrefixer->prefixDirectoryPath($path);\n        $items = [];\n\n        for ($i = 0; $i < $archive->numFiles; $i++) {\n            $stats = $archive->statIndex($i);\n            // @codeCoverageIgnoreStart\n            if ($stats === false) {\n                continue;\n            }\n            // @codeCoverageIgnoreEnd\n\n            $itemPath = $stats['name'];\n\n            if (\n                $location === $itemPath\n                || ($deep && $location !== '' && ! str_starts_with($itemPath, $location))\n                || ($deep === false && ! $this->isAtRootDirectory($location, $itemPath))\n            ) {\n                continue;\n            }\n\n            $items[] = $this->isDirectoryPath($itemPath)\n                ? new DirectoryAttributes(\n                    $this->pathPrefixer->stripDirectoryPrefix($itemPath),\n                    null,\n                    $stats['mtime']\n                )\n                : new FileAttributes(\n                    $this->pathPrefixer->stripPrefix($itemPath),\n                    $stats['size'],\n                    null,\n                    $stats['mtime']\n                );\n        }\n\n        $archive->close();\n\n        return $this->yieldItemsFrom($items);\n    }\n\n    private function yieldItemsFrom(array $items): Generator\n    {\n        yield from $items;\n    }\n\n    public function move(string $source, string $destination, Config $config): void\n    {\n        try {\n            $this->ensureParentDirectoryExists($destination, $config);\n        } catch (Throwable $exception) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);\n        }\n\n        $archive = $this->zipArchiveProvider->createZipArchive();\n\n        if ($archive->locateName($this->pathPrefixer->prefixPath($destination)) !== false) {\n            if ($source === $destination) {\n                //update the config of the file\n                $this->copy($source, $destination, $config);\n\n                return;\n            }\n\n            $this->delete($destination);\n            $this->copy($source, $destination, $config);\n            $this->delete($source);\n\n            return;\n        }\n\n        $renamed = $archive->renameName(\n            $this->pathPrefixer->prefixPath($source),\n            $this->pathPrefixer->prefixPath($destination)\n        );\n        if ($renamed === false) {\n            throw UnableToMoveFile::fromLocationTo($source, $destination);\n        }\n    }\n\n    public function copy(string $source, string $destination, Config $config): void\n    {\n        try {\n            $readStream = $this->readStream($source);\n            $this->writeStream($destination, $readStream, $config);\n        } catch (Throwable $exception) {\n            if (isset($readStream)) {\n                @fclose($readStream);\n            }\n\n            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);\n        }\n    }\n\n    private function ensureParentDirectoryExists(string $path, Config $config): void\n    {\n        $dirname = dirname($path);\n\n        if ($dirname === '' || $dirname === '.') {\n            return;\n        }\n\n        $this->ensureDirectoryExists($dirname, $config);\n    }\n\n    private function ensureDirectoryExists(string $dirname, Config $config): void\n    {\n        $visibility = $config->get(Config::OPTION_DIRECTORY_VISIBILITY);\n        $archive = $this->zipArchiveProvider->createZipArchive();\n        $prefixedDirname = $this->pathPrefixer->prefixDirectoryPath($dirname);\n        $parts = array_filter(explode('/', trim($prefixedDirname, '/')));\n        $dirPath = '';\n\n        foreach ($parts as $part) {\n            $dirPath .= $part . '/';\n            $info = $archive->statName($dirPath);\n\n            if ($info === false && $archive->addEmptyDir($dirPath) === false) {\n                throw UnableToCreateDirectory::atLocation($dirname);\n            }\n\n            if ($visibility === null) {\n                continue;\n            }\n\n            if ( ! $this->setVisibilityAttribute($dirPath, $visibility, $archive)) {\n                $archive->close();\n                throw UnableToCreateDirectory::atLocation($dirname, 'Unable to set visibility.');\n            }\n        }\n\n        $archive->close();\n    }\n\n    private function isDirectoryPath(string $path): bool\n    {\n        return str_ends_with($path, '/');\n    }\n\n    private function isAtRootDirectory(string $directoryRoot, string $path): bool\n    {\n        $dirname = dirname($path);\n\n        if ('' === $directoryRoot && '.' === $dirname) {\n            return true;\n        }\n\n        return $directoryRoot === (rtrim($dirname, '/') . '/');\n    }\n\n    private function setVisibilityAttribute(string $statsName, string $visibility, ZipArchive $archive): bool\n    {\n        $visibility = $this->isDirectoryPath($statsName)\n            ? $this->visibility->forDirectory($visibility)\n            : $this->visibility->forFile($visibility);\n\n        return $archive->setExternalAttributesName($statsName, ZipArchive::OPSYS_UNIX, $visibility << 16);\n    }\n}\n"
  },
  {
    "path": "src/ZipArchive/ZipArchiveAdapterTestCase.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\ZipArchive;\n\nuse Generator;\nuse League\\Flysystem\\AdapterTestUtilities\\FilesystemAdapterTestCase;\nuse League\\Flysystem\\Config;\nuse League\\Flysystem\\FilesystemAdapter;\nuse League\\Flysystem\\UnableToCopyFile;\nuse League\\Flysystem\\UnableToCreateDirectory;\nuse League\\Flysystem\\UnableToDeleteDirectory;\nuse League\\Flysystem\\UnableToDeleteFile;\nuse League\\Flysystem\\UnableToMoveFile;\nuse League\\Flysystem\\UnableToSetVisibility;\nuse League\\Flysystem\\UnableToWriteFile;\nuse League\\Flysystem\\Visibility;\n\nuse function iterator_to_array;\n\n/**\n * @group zip\n */\nabstract class ZipArchiveAdapterTestCase extends FilesystemAdapterTestCase\n{\n    private const ARCHIVE = __DIR__ . '/test.zip';\n\n    /**\n     * @var StubZipArchiveProvider\n     */\n    private static $archiveProvider;\n\n    protected function setUp(): void\n    {\n        static::$adapter = static::createFilesystemAdapter();\n        static::removeZipArchive();\n        parent::setUp();\n    }\n\n    public static function tearDownAfterClass(): void\n    {\n        static::removeZipArchive();\n    }\n\n    protected function tearDown(): void\n    {\n        static::removeZipArchive();\n    }\n\n    protected static function createFilesystemAdapter(): FilesystemAdapter\n    {\n        static::$archiveProvider = new StubZipArchiveProvider(self::ARCHIVE);\n\n        return new ZipArchiveAdapter(self::$archiveProvider, static::getRoot());\n    }\n\n    abstract protected static function getRoot(): string;\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_create_the_parent_directory(): void\n    {\n        $this->expectException(UnableToCreateParentDirectory::class);\n\n        (new ZipArchiveAdapter(new StubZipArchiveProvider('/no-way/this/will/work')))\n            ->write('haha', 'lol', new Config());\n    }\n\n    /**\n     * @test\n     */\n    public function not_being_able_to_write_a_file_because_the_parent_directory_could_not_be_created(): void\n    {\n        self::$archiveProvider->stubbedZipArchive()->failNextDirectoryCreation();\n\n        $this->expectException(UnableToWriteFile::class);\n\n        $this->adapter()->write('directoryName/is-here/filename.txt', 'contents', new Config());\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider scenariosThatCauseWritesToFail\n     */\n    public function scenarios_that_cause_writing_a_file_to_fail(callable $scenario): void\n    {\n        $this->runScenario($scenario);\n\n        $this->expectException(UnableToWriteFile::class);\n\n        $this->runScenario(function () {\n            $handle = stream_with_contents('contents');\n            $this->adapter()->writeStream('some/path.txt', $handle, new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC]));\n            is_resource($handle) && @fclose($handle);\n        });\n    }\n\n    public static function scenariosThatCauseWritesToFail(): Generator\n    {\n        yield \"writing a file fails when writing\" => [function () {\n            static::$archiveProvider->stubbedZipArchive()->failNextWrite();\n        }];\n\n        yield \"writing a file fails when setting visibility\" => [function () {\n            static::$archiveProvider->stubbedZipArchive()->failWhenSettingVisibility();\n        }];\n\n        yield \"writing a file fails to get the stream contents\" => [function () {\n            mock_function('stream_get_contents', false);\n        }];\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_delete_a_file(): void\n    {\n        $this->givenWeHaveAnExistingFile('path.txt');\n        static::$archiveProvider->stubbedZipArchive()->failNextDeleteName();\n        $this->expectException(UnableToDeleteFile::class);\n\n        $this->adapter()->delete('path.txt');\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_directory(): void\n    {\n        $this->givenWeHaveAnExistingFile('a.txt');\n        $this->givenWeHaveAnExistingFile('one/a.txt');\n        $this->givenWeHaveAnExistingFile('one/b.txt');\n        $this->givenWeHaveAnExistingFile('two/a.txt');\n\n        $items = iterator_to_array($this->adapter()->listContents('', true));\n        $this->assertCount(6, $items);\n\n        $this->adapter()->deleteDirectory('one');\n\n        $items = iterator_to_array($this->adapter()->listContents('', true));\n        $this->assertCount(3, $items);\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_prefixed_directory(): void\n    {\n        $this->givenWeHaveAnExistingFile('a.txt');\n        $this->givenWeHaveAnExistingFile('/one/a.txt');\n        $this->givenWeHaveAnExistingFile('one/b.txt');\n        $this->givenWeHaveAnExistingFile('two/a.txt');\n\n        $items = iterator_to_array($this->adapter()->listContents('', true));\n        $this->assertCount(6, $items);\n\n        $this->adapter()->deleteDirectory('one');\n\n        $items = iterator_to_array($this->adapter()->listContents('', true));\n        $this->assertCount(3, $items);\n    }\n\n    /**\n     * @test\n     */\n    public function list_root_directory(): void\n    {\n        $this->givenWeHaveAnExistingFile('a.txt');\n        $this->givenWeHaveAnExistingFile('one/a.txt');\n        $this->givenWeHaveAnExistingFile('one/b.txt');\n        $this->givenWeHaveAnExistingFile('two/a.txt');\n\n        $this->assertCount(6, iterator_to_array($this->adapter()->listContents('', true)));\n        $this->assertCount(3, iterator_to_array($this->adapter()->listContents('', false)));\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_create_a_directory(): void\n    {\n        static::$archiveProvider->stubbedZipArchive()->failNextDirectoryCreation();\n\n        $this->expectException(UnableToCreateDirectory::class);\n\n        $this->adapter()->createDirectory('somewhere', new Config);\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_create_a_directory_because_setting_visibility_fails(): void\n    {\n        static::$archiveProvider->stubbedZipArchive()->failWhenSettingVisibility();\n\n        $this->expectException(UnableToCreateDirectory::class);\n\n        $this->adapter()->createDirectory('somewhere', new Config([Config::OPTION_DIRECTORY_VISIBILITY => Visibility::PRIVATE]));\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_delete_a_directory(): void\n    {\n        static::$archiveProvider->stubbedZipArchive()->failWhenDeletingAnIndex();\n\n        $this->givenWeHaveAnExistingFile('here/path.txt');\n\n        $this->expectException(UnableToDeleteDirectory::class);\n\n        $this->adapter()->deleteDirectory('here');\n    }\n\n    /**\n     * @test\n     */\n    public function setting_visibility_on_a_directory(): void\n    {\n        $adapter = $this->adapter();\n        $adapter->createDirectory('pri-dir', new Config([Config::OPTION_DIRECTORY_VISIBILITY => Visibility::PRIVATE]));\n        $adapter->createDirectory('pub-dir', new Config([Config::OPTION_DIRECTORY_VISIBILITY => Visibility::PUBLIC]));\n\n        $this->expectNotToPerformAssertions();\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_move_a_file(): void\n    {\n        $this->givenWeHaveAnExistingFile('somewhere/here.txt');\n\n        static::$archiveProvider->stubbedZipArchive()->failNextDirectoryCreation();\n\n        $this->expectException(UnableToMoveFile::class);\n\n        $this->adapter()->move('somewhere/here.txt', 'to-here/path.txt', new Config);\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_copy_a_file(): void\n    {\n        $this->givenWeHaveAnExistingFile('here.txt');\n\n        static::$archiveProvider->stubbedZipArchive()->failNextWrite();\n\n        $this->expectException(UnableToCopyFile::class);\n\n        $this->adapter()->copy('here.txt', 'here.txt', new Config);\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_set_visibility_because_the_file_does_not_exist(): void\n    {\n        $this->expectException(UnableToSetVisibility::class);\n\n        $this->adapter()->setVisibility('path.txt', Visibility::PUBLIC);\n    }\n\n    /**\n     * @test\n     */\n    public function deleting_a_directory_with_files_in_it(): void\n    {\n        $this->givenWeHaveAnExistingFile('nested/path-a.txt');\n        $this->givenWeHaveAnExistingFile('nested/path-b.txt');\n\n        $this->adapter()->deleteDirectory('nested');\n        $listing = iterator_to_array($this->adapter()->listContents('', true));\n\n        self::assertEquals([], $listing);\n    }\n\n    /**\n     * @test\n     */\n    public function failing_to_set_visibility_because_setting_it_fails(): void\n    {\n        $this->givenWeHaveAnExistingFile('path.txt');\n        static::$archiveProvider->stubbedZipArchive()->failWhenSettingVisibility();\n\n        $this->expectException(UnableToSetVisibility::class);\n\n        $this->adapter()->setVisibility('path.txt', Visibility::PUBLIC);\n    }\n\n    /**\n     * @test\n     *\n     * @fixme Move to FilesystemAdapterTestCase once all adapters pass\n     */\n    public function moving_a_file_and_overwriting(): void\n    {\n        $this->runScenario(function () {\n            $adapter = $this->adapter();\n            $adapter->write(\n                'source.txt',\n                'contents to be moved',\n                new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])\n            );\n            $adapter->write(\n                'destination.txt',\n                'contents to be overwritten',\n                new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC])\n            );\n            $adapter->move('source.txt', 'destination.txt', new Config());\n            $this->assertFalse(\n                $adapter->fileExists('source.txt'),\n                'After moving a file should no longer exist in the original location.'\n            );\n            $this->assertTrue(\n                $adapter->fileExists('destination.txt'),\n                'After moving, a file should be present at the new location.'\n            );\n            $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('destination.txt')->visibility());\n            $this->assertEquals('contents to be moved', $adapter->read('destination.txt'));\n        });\n    }\n\n    protected static function removeZipArchive(): void\n    {\n        if ( ! file_exists(self::ARCHIVE)) {\n            return;\n        }\n\n        unlink(self::ARCHIVE);\n    }\n}\n"
  },
  {
    "path": "src/ZipArchive/ZipArchiveException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\ZipArchive;\n\nuse League\\Flysystem\\FilesystemException;\n\ninterface ZipArchiveException extends FilesystemException\n{\n}\n"
  },
  {
    "path": "src/ZipArchive/ZipArchiveProvider.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace League\\Flysystem\\ZipArchive;\n\nuse ZipArchive;\n\ninterface ZipArchiveProvider\n{\n    public function createZipArchive(): ZipArchive;\n}\n"
  },
  {
    "path": "src/ZipArchive/composer.json",
    "content": "{\n    \"name\": \"league/flysystem-ziparchive\",\n    \"description\": \"ZIP filesystem adapter for Flysystem.\",\n    \"keywords\": [\"filesystem\", \"flysystem\", \"zip\", \"files\", \"file\"],\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"autoload\": {\n        \"psr-4\": {\n            \"League\\\\Flysystem\\\\ZipArchive\\\\\": \"\"\n        }\n    },\n    \"require\": {\n        \"php\": \"^8.0.2\",\n        \"ext-zip\": \"*\",\n        \"league/flysystem\": \"^3.0.0\",\n        \"league/mime-type-detection\": \"^1.0.0\"\n    },\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Frank de Jonge\",\n            \"email\": \"info@frankdejonge.nl\"\n        }\n    ]\n}\n"
  },
  {
    "path": "test_files/.gitignore",
    "content": ".data/\n"
  },
  {
    "path": "test_files/sftp/id_rsa",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0cx7jSu+FZQJFmz0+vlbo1I+awlaxvqZUseuPvX/YA32c3hE\nnr0OpEMhe/Pvs4jk/BiPp5CfITFe6ykxUvNlvxh/9EOsnUFaFrfyLhYn3TicW6lv\nzSJmJ7tydW/L0zpMPliItbjtXItxHOyoVcl20+DT+LZWlRRnnuSDTC1Oc6vvLosw\nb4lOTNVB2kV7W9urU6W+OilcERwjjQpiIM4mkwg2dX0aPWS5o0N9tYi3+k6gStqj\nDfcTw3L8tTWeDcWrblvFaknFfBzkeXx9dTyrAYw3ii3LZQ6hA5UK8tUgwUVThTh+\nQBhzhmn14Ljfp1fa3vgljY9yn5Zw+Drhbtn4TwIDAQABAoIBACaZstnEhJK/y/Q+\nU8yheITSKv3SmMsnbHJYnuyiojvwFbolFKsIKdt7JnwB48Zql4bylevEpiKbTNWD\nnLmgYsYIIfK1SNseHQ81BPAJz4faVJpg0FszywvgZyzIRv40KbcG3xBgV/vBBCzI\nNiiiiqRtJ1MJaWDAglgvvyCS7W5GjHue74o/wPugyvIi7zmMqMmZLg3zz/YF3zxU\nh3JGFnw/TlN3yDJzdAOFiLGpSRD/bMcBJyWc9Oa2LQj+Emkc7dojmB/IJ7wJ9iuo\nNi2/EJIQoFtVYxK6r+nRVuvT6/siMi7Ok4EL7PANVNVA7s7e+rmZX3NjDiGnvh5V\nDyxdT5ECgYEA/UFm3qxLiSQwNGuQLpCO9WR+4eeZPthNI1ySkcAnGyrmxX70ws7M\n5j1jOIYUhf8i1ALUu++sBfvd+OhD0yeRgFvqrCdeHdFnAdgOtQDUGfU9bh1YpEX0\n4A7zIxK7TBi26hXb/fTu7YwDtv58W1BtOOfo9ZKOWPu1ZQbEG5fp9pMCgYEA1BKF\nMRWshqBOSqRhyrl34wXTr6EI1XcwIDNyAGJnQFlbxWcCuPYCkF+ENZOe8D6soXiZ\ntTW6zSkM+UmsNgEYjD5b0eWgd0yUdrOi/Z4JZ16SlroeLYfbqVYY6NPE6XEuzZ9p\nXEB5WedUGIdU2JZQ422m7BxpJBIRtlbhG1Oi8NUCgYEA9Y3MaGs2ciqccrc4fW28\nr0JZpEAi3kRrxrWjh56ATF80kpmeSKSrFzK+WbfnfmT7KAX2rqKccNDdUNIjsUDU\nW1jEGVeyccbv0WHkIKxE+0ZF4daic+VAoV7dcExhPk9YS3AWdg5e/ASeNXhaq084\nF80Em9cWHkEwiFwfGYIaX/ECgYEAnUqfPyi0LaX4a6RAY/vrz5Yiy8DErI8aQsfl\nZiOWMUQVrPQaMNVGUY6GoLY8zDOwFpM8bgrL4h7wYHUkJWnqqxoVQDjwK4vBEclq\nunDcyK58Sw8AEwURBye0kft/sSUhcaEqpCGt3+CTnx3A8GOM2yIZDEaGNRqxyGvn\nyjzePYECgYBqr0OQJUDUOGcL+V5z8wCaLN2bbWK/bukaGe3ZU7W2kWhNvVL0SBNC\ndUcxLp5ELOj8wFHmTvZv1q2aszIY2CQhnc1qGYCh34UAK14DkTvSYie9XcGPhkWK\nUC9VLorkPbPAZipbBs9Dt079ub3HRyeXX4b0VHHKzu2XVGXVEOXO6A==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "test_files/sftp/id_rsa.pub",
    "content": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDRzHuNK74VlAkWbPT6+VujUj5rCVrG+plSx64+9f9gDfZzeESevQ6kQyF78++ziOT8GI+nkJ8hMV7rKTFS82W/GH/0Q6ydQVoWt/IuFifdOJxbqW/NImYnu3J1b8vTOkw+WIi1uO1ci3Ec7KhVyXbT4NP4tlaVFGee5INMLU5zq+8uizBviU5M1UHaRXtb26tTpb46KVwRHCONCmIgziaTCDZ1fRo9ZLmjQ321iLf6TqBK2qMN9xPDcvy1NZ4NxatuW8VqScV8HOR5fH11PKsBjDeKLctlDqEDlQry1SDBRVOFOH5AGHOGafXguN+nV9re+CWNj3KflnD4OuFu2fhP your_email@example.com\n"
  },
  {
    "path": "test_files/sftp/ssh_host_ed25519_key",
    "content": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACCPTP3g9O8X9Ir5KwrSVkYRqgCZFFt4i7oHBKyy31HiCQAAALBMCYEjTAmB\nIwAAAAtzc2gtZWQyNTUxOQAAACCPTP3g9O8X9Ir5KwrSVkYRqgCZFFt4i7oHBKyy31HiCQ\nAAAEAsRUm7efqtLmLIUwhaHzPnKf+hUubVc+xr49XcILRW5Y9M/eD07xf0ivkrCtJWRhGq\nAJkUW3iLugcErLLfUeIJAAAAKmZyYW5rZGVqb25nZUBGcmFuay1kZS1Kb25nZXMtTUJQMj\nAxNS5sb2NhbAECAw==\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  {
    "path": "test_files/sftp/ssh_host_ed25519_key.pub",
    "content": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII9M/eD07xf0ivkrCtJWRhGqAJkUW3iLugcErLLfUeIJ frankdejonge@Frank-de-Jonges-MBP2015.local\n"
  },
  {
    "path": "test_files/sftp/ssh_host_rsa_key",
    "content": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAgEA48TBtykiBjI65ZBWCpOHk86EZrIxBFxb5WpL0ujBSrp05UJ8eKeX\nmoEefhR4Vqll9Oi+ejmKXUvvkH1M638N35SFq+S3HR8TsMvgn5MaWO5HH3F5zzzLzyZjtZ\nH/K6mroMhG1b/DZoG3s4A5jN1i2ByRJS9d65q+Txd9ItlEifiCs7YYDS/5CkMWp3I6+i64\nCmARpMUvfVwpclFFIaxnlevymRIwSxzUoZxGuLYR2+QUx/4sbx41CIOLwEWrxf0oq9+DS3\neyp+av4itLo47ndLfNEFexUHSPE8IofGuKAIUkHrPMVwLoFT+FbIDVH6vnU9XawIpbHFev\nju2Mopb3DS9CKLLziQhBSxwCgsJmFyTqzkPZ3YeSsaz22o+vW98HRnp6TTtv+iefArNRPr\ngikBdFEjK8jWwZWV6MyeU5u4fZngMghV3DOfBjTlhrONI50iicM1sNKEvUqzEqCL/fs+zX\nV04aeDyNw+O6Xy1uIT+2Ozo5bHMoeyqqM33Zb7CB+polZQckFrXZAS59KLRYy2EpNQVzNY\nUThywQDPk5fkJ1CczHwlAzLlNZdxnsG69+JYGWlqKu//dT4AJPuYNDVW7SxPec8+yYq7uu\nDZnsnp8in/0tiyr4AS4fnAOszIndqRwwzqm13BVIDNJbdwgJTRDC+3wBT6hjnPC0ktACtN\nsAAAdo+KBGYPigRmAAAAAHc3NoLXJzYQAAAgEA48TBtykiBjI65ZBWCpOHk86EZrIxBFxb\n5WpL0ujBSrp05UJ8eKeXmoEefhR4Vqll9Oi+ejmKXUvvkH1M638N35SFq+S3HR8TsMvgn5\nMaWO5HH3F5zzzLzyZjtZH/K6mroMhG1b/DZoG3s4A5jN1i2ByRJS9d65q+Txd9ItlEifiC\ns7YYDS/5CkMWp3I6+i64CmARpMUvfVwpclFFIaxnlevymRIwSxzUoZxGuLYR2+QUx/4sbx\n41CIOLwEWrxf0oq9+DS3eyp+av4itLo47ndLfNEFexUHSPE8IofGuKAIUkHrPMVwLoFT+F\nbIDVH6vnU9XawIpbHFevju2Mopb3DS9CKLLziQhBSxwCgsJmFyTqzkPZ3YeSsaz22o+vW9\n8HRnp6TTtv+iefArNRPrgikBdFEjK8jWwZWV6MyeU5u4fZngMghV3DOfBjTlhrONI50iic\nM1sNKEvUqzEqCL/fs+zXV04aeDyNw+O6Xy1uIT+2Ozo5bHMoeyqqM33Zb7CB+polZQckFr\nXZAS59KLRYy2EpNQVzNYUThywQDPk5fkJ1CczHwlAzLlNZdxnsG69+JYGWlqKu//dT4AJP\nuYNDVW7SxPec8+yYq7uuDZnsnp8in/0tiyr4AS4fnAOszIndqRwwzqm13BVIDNJbdwgJTR\nDC+3wBT6hjnPC0ktACtNsAAAADAQABAAACAQCESra9HK3/bVNaHNBsyi2YAv5R67OetcpG\nYMvzj28daVkWA9zp82WRvucoEdmndDKc4kYoFZ2w/LcDdFOmAKDdOJW/NlPJHVDBgllQNg\n+6kYNL1wwJ+2ThR4noXwkXoi/mbgz+f6gNtNAu+Q30LG4J2eXP9EgX3UQmCh2LjShK/sVj\nfiNQHYoHlNnmnel1gIcyt4Pn8QPZSxtjo6KEoW9025uHntHf/rnduDg3dsC+uCX91zqVu7\nTP4h/cqFrR322tDmBjB/4DmXCU69K+B/WVjGAV2ulJMrobns0HHysDjFFjZ8kKzMxh8wga\n8mVXRPBSeEbbSEENIDz+xijGEusga3EK3I4LMNaAN47oulrVM++oF1pHL74jiEAC1SGHTv\n0ZOZdCDmdmPyxCC9ruvJmQb3IbvoVyROcrHd4AvYSKnRqE2ljzLnFHG+Qp13meoXNz8Bup\nNO9ra+HJYWu6QgqHnPvwrIP5NdmwwSRWunglOvVSO2c11X7Li/NBkLLLk9wX1w9awAPfTf\nnUGCFnKi+1ianXD94zEG0FyXImN1eRK2lgs8ul6wGr3qV5B3sv7llgyWF5ZKLLj9iQ79M5\nOKret5evybFbEbIOWbRTTcREfmSLqcKv2g6ZJpBNI8z4bz0jgc4hEVcMJlyiH52v3yMePh\nf7tXvv62Ws9IHLNhnSkQAAAQEApXAZ5Ns3aUOuCVvK3mEUWuQEsh/aM+rdrthX+zrw7Ma7\nkw2O6zh6VviP5BV+8qG7sx1KyK9b0u+/qGDy4pMHN8SjNq1W+pTZ4FHkA/1UKI0kTvTwq/\nWQvO1pBxLbx77gl5FuBjysPljnvMh3+l9QOFX4iwjFoyep1FgammuD4hnSR1yWuF3H/2xc\nsb75LnXdaEDm5ALyauTwmzdufTCaQmq19y8hZ5Kbhxd/Pao160tm38n2gektN458AbGxri\n8tHSwpj4+3PSOwFeFalxk/jO8daye4PluoqK+O60Z5NWqdDDaBdvxEMcj9sddUQaQ0BzmK\nFci3Hl6vS9TzBUWNOQAAAQEA/hDQkIH3ZIdZYBWIsinTKoztovUZHfHZihkBxcR0IbKZkn\n7HGgtNtv1QshSvpzMp7HsAT45SqzIbN4ldNh0aSRmoOb2/pVNVNeJrWR0B+aS9hya6bAOY\nAiZlfMMo+2cU+gJzGD50tlu7ge4hT3YkN3vmoDyJZ2p7DxOioQQ10D5Pno8ZGh8kZX3t5d\nF+5GwPnsYXsi/iMgw18ggPTBPwAP+TeUsdP8Ae93h33LgCOkUfXeq/HmQ0jTUT84lxd9tW\n2N/w6P/2SYa85oFVa2Y+u0qRJOLf6zJKTOH/ERV9IJYGLRzyxybQuo4iqw9V+kP13KkC2w\n6vh2RcmEwXmxXRyQAAAQEA5YCwEjBqYLlPzQ3lOf75SbaT5+CIiyNy0P0ZUvpBl+URJkGC\nm8/AA3naWylEP/oUJRUkv21qXB8EEL62404nhNVtfDGX1mpht8H4N9lcXAdNFg1SWmqnqb\n58PTODzM4n9CVjqnqqlTGQi1ph77RLDVXc29UooopLtYFsttiFBvv8Cs/cKx3t1ECQRklU\nhjPSdijeRTtv5x604uXt6mObYIGMAC87OSP4iaY00Dz17lnJ9m9UK5BCX+44sIEf3IQ9Ad\njfaeN5UoUTv+y/fB419baC+dlIR9+SaEFqNgMHQqpyZApWMhCf99oNQhJ9XDjVEtKtXnhc\n5Lh4tYyNGMsDgwAAACpmcmFua2Rlam9uZ2VARnJhbmstZGUtSm9uZ2VzLU1CUDIwMTUubG\n9jYWwBAgMEBQYH\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  {
    "path": "test_files/sftp/ssh_host_rsa_key.pub",
    "content": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDjxMG3KSIGMjrlkFYKk4eTzoRmsjEEXFvlakvS6MFKunTlQnx4p5eagR5+FHhWqWX06L56OYpdS++QfUzrfw3flIWr5LcdHxOwy+CfkxpY7kcfcXnPPMvPJmO1kf8rqaugyEbVv8NmgbezgDmM3WLYHJElL13rmr5PF30i2USJ+IKzthgNL/kKQxancjr6LrgKYBGkxS99XClyUUUhrGeV6/KZEjBLHNShnEa4thHb5BTH/ixvHjUIg4vARavF/Sir34NLd7Kn5q/iK0ujjud0t80QV7FQdI8Twih8a4oAhSQes8xXAugVP4VsgNUfq+dT1drAilscV6+O7YyilvcNL0IosvOJCEFLHAKCwmYXJOrOQ9ndh5KxrPbaj69b3wdGenpNO2/6J58Cs1E+uCKQF0USMryNbBlZXozJ5Tm7h9meAyCFXcM58GNOWGs40jnSKJwzWw0oS9SrMSoIv9+z7NdXThp4PI3D47pfLW4hP7Y7Ojlscyh7KqozfdlvsIH6miVlByQWtdkBLn0otFjLYSk1BXM1hROHLBAM+Tl+QnUJzMfCUDMuU1l3Gewbr34lgZaWoq7/91PgAk+5g0NVbtLE95zz7Jiru64NmeyenyKf/S2LKvgBLh+cA6zMid2pHDDOqbXcFUgM0lt3CAlNEML7fAFPqGOc8LSS0AK02w== frankdejonge@Frank-de-Jonges-MBP2015.local\n"
  },
  {
    "path": "test_files/sftp/sshd_custom_configs.sh",
    "content": "#!/bin/bash\n\ncat <<'EOF' >> /etc/ssh/sshd_config\n\nKexAlgorithms curve25519-sha256\nCiphers aes256-gcm@openssh.com\nMACs hmac-sha2-256-etm@openssh.com\nHostKeyAlgorithms ssh-ed25519\n\nEOF\n"
  },
  {
    "path": "test_files/sftp/unknown.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAxZQy1FnuqhoZsiqvFanN5tZRL6GVeCBeoLroGBEggJYVDLw2\nnPaXCDOaDoVzs1PkLoKhttA6g4l4eHZgDL1b6QTxRnDbkdOxKJHGEapk+WZk23yq\nQmU4thDdmgj0clJfS3aTbF7hGWpxrv9zB5saWwgWB/fBeYXu+kXuuO1UJF+F0QGt\n6TVVMc6o3NaI7Cq1qoMvt7XGN5LegwfNWRGxaKTL1yn7pkljhkPLOoGedBemieya\naQhV+dwzOVeBAXGKSmtiUwto8OMYl/suptMdo+91cLMRJkgXNQ5gA+4niqja5HGW\n2Dw8vXja2Spp4+gWWh5OpXwd2pNnJScNINL30wIDAQABAoIBAHzBD7s/sdAcPN9f\nzj+qgUVhS8/8gilgnv90JPqVTeWDXnU1HnLLzR+znXHP1/eCYBDyEPQi1N+bXMML\nU6iXpEIlCcfFmQ6iETmhmeQrqChF/CcOt17HFSD400PgpaDN3DgE/h8uZYmryW6L\nA3HpAKI8H9UWHkcCR5wlrg98Y2W3AacHMwRG8da7QxLcGd10+1lFLuPXTu7LwDf0\nu/+Jq9IPwm8WemGiuuOEQ06r7y1s929pA7fWSsn8DlVBIFnPHCtGKpiDfAyEHmCh\n/LUU6Z5Cl9186sGohu5hWUA+qTMAHw9IR0TbWxF+tAEX8qL5z/qH/surRvzfeRCi\nBooQB4ECgYEA99xz7AWMUzu6J4FSm29M2AS989Yz0XAgGIYs69pxoaRR5azRffY+\nF6wjBpeSI5BElu1DAublhv5hpCgdcw6X2EfMnKvP1jtSbv+DOdR0p80LVAnG7tb3\nstSgNwMsGdBpTCghHKUvOUGDU3D3c9O8jCrrJd9Unu4gj2/xbVuC45MCgYEAzBER\nXeuKuwrN0HmLCZ7AJ1aq7A60UKJjMicdXD5n95ePwHQ32s+hmrcGfUOjcZoWc1Dp\nyZaZvqFDYu52ZJ1QbNZqjBQJ8sjERSxNlV8DZe1I3ohXvsRQ4I6g8/HRgSl0CVqg\n+LeJA7n18+SrhhCWhY+dggV1m4qCNcWI7FCQwsECgYEAw+V7xT35U0twbIq8lFba\nQB03WEGiwNRCub9KP7ptdtjdVY5KIKj/GEyXfj1LZko+u56YCPIe1Ju25jxCUk5l\nWq4cnHL6mBJYq5vMxmcRMBJR8sCrdtd1++QrIG+kal6a6nMJAI/ZjAIoXkl5ehUN\n/yZopY0mX1pLZ7KM+OaLw3sCgYEAvaACllbA9Gvmspmu5IKbJjL34yEK137+VGVa\neBQZgk5ZK0oTeQXFssHuisomf/Lid8exZzzFowmxV6YlZ/ty96ALJB2e3PdIwsqX\nUX0X6EgllXv2pXNBgFmpIOYNe0ts4yBPQq8x57+O2FMePBb/+B5rC55NGfsMYjEr\nugRncEECgYB9Wrnp4/WpTwcSJTJXWB+FJaMmcXqbFLk/AI1HbRXJFmR/ssK1gugQ\nY3vevvRMSJkYkd1Zmbp9x8ZAGKzR/yeT8lQ6gY/xYq1fYLKKKtzHdwTMCVIVDgSf\n/v9Y8mLylcgr7ZjEo8xMTnb98ozSSFPBn2jq8c31AsQxXaIMK0BE7g==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "test_files/sftp/users.conf",
    "content": "foo:pass:1001:100:upload\nbar:pass:1001:100:upload\n"
  },
  {
    "path": "test_files/toxiproxy/toxiproxy.json",
    "content": "[\n  {\n    \"name\": \"sftp\",\n    \"listen\": \"[::]:8222\",\n    \"upstream\": \"sftp:22\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"ftp\",\n    \"listen\": \"[::]:8121\",\n    \"upstream\": \"ftp:21\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"ftpd\",\n    \"listen\": \"[::]:8122\",\n    \"upstream\": \"ftpd:21\",\n    \"enabled\": true\n  }\n]\n"
  },
  {
    "path": "test_files/wait_for_ftp.php",
    "content": "<?php\n\nuse League\\Flysystem\\Ftp\\FtpConnectionOptions;\nuse League\\Flysystem\\Ftp\\FtpConnectionProvider;\n\ninclude __DIR__ . '/../vendor/autoload.php';\n\n$options = FtpConnectionOptions::fromArray([\n   'host' => 'localhost',\n   'port' => (int) ($argv[1] ?? 2122),\n   'root' => '/',\n   'username' => 'foo',\n   'password' => 'pass',\n]);\n\n$provider = new FtpConnectionProvider();\n$start = time();\n$connected = false;\n\nwhile (time() - $start < 60) {\n    try {\n        $provider->createConnection($options);\n        $connected = true;\n        break;\n    } catch (Throwable $exception) {\n        if (time() - $start < 30) {\n            fwrite(STDOUT, \"Exception while trying to connect:'\\n\");\n            fwrite(STDOUT, (string) $exception);\n            fwrite(STDOUT, \"\\n\\n\");\n        }\n        usleep(10000);\n    }\n}\n\nif ( ! $connected) {\n    fwrite(STDERR, \"Unable to start FTP server.\\n\");\n    exit(1);\n}\n\nfwrite(STDOUT, \"Detected FTP server successfully.\\n\");\n"
  },
  {
    "path": "test_files/wait_for_sftp.php",
    "content": "<?php\n\nuse League\\Flysystem\\PhpseclibV2\\SftpConnectionProvider as V2Provider;\nuse League\\Flysystem\\PhpseclibV3\\SftpConnectionProvider as V3Provider;\nuse phpseclib3\\Net\\SFTP;\n\ninclude __DIR__ . '/../vendor/autoload.php';\n\n$providerName = class_exists(SFTP::class) ? V3Provider::class : V2Provider::class;\n$connectionProvider = $providerName::fromArray(\n    [\n        'host' => 'localhost',\n        'username' => 'foo',\n        'password' => 'pass',\n        'port' => 2222,\n    ]\n);\n\n$start = time();\n$connected = false;\n\nwhile (time() - $start < 60) {\n    try {\n        $connectionProvider->provideConnection();\n        $connected = true;\n        break;\n    } catch (Throwable $exception) {\n        echo($exception);\n        usleep(10000);\n    }\n}\n\nif ( ! $connected) {\n    fwrite(STDERR, \"Unable to start SFTP server.\\n\");\n    exit(1);\n}\n\nfwrite(STDOUT, \"Detected SFTP server successfully.\\n\");\n"
  }
]