Compare commits
16 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 516350ecf6 | |||
| 68369b615c | |||
| 117f70510f | |||
| 15cc0cd1ec | |||
| 0552f17521 | |||
| 969ec1363c | |||
| bbcc9c6353 | |||
| 8dc7434133 | |||
| d3ec44f871 | |||
| 9f3b0f22c1 | |||
| a7edc98c0d | |||
| 908c988305 | |||
| 24e5d6b270 | |||
| 934c6aa4b1 | |||
| 50f74301cd | |||
| 4372a471c2 |
23 changed files with 851 additions and 183 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
|||
workspace.code-workspace
|
||||
vendor
|
||||
node_modules/
|
||||
.vscode
|
||||
auth.json
|
||||
composer.lock
|
||||
|
|
|
|||
115
CHANGELOG.md
115
CHANGELOG.md
|
|
@ -1,80 +1,99 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to `admin-secure-laravel` will be documented in this file.
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
- This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
- Commits respect [Conventionnal commits](https://www.conventionalcommits.org/en/v1.0.0/) & use [Gitmoji](https://gitmoji.dev/).
|
||||
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
- This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
- Commits respect [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) & use [Gitmoji](https://gitmoji.dev/).
|
||||
|
||||
## **[v5.1.0] - 17/04/2026**
|
||||
## [1.0.3] - 2026-11-04
|
||||
|
||||
### Added
|
||||
### 🚀 Features
|
||||
|
||||
- chore: :arrow_up: Added Laravel 13 compatibility
|
||||
- *(StorageServiceConfig)* Support scoped driver with boot-time underlying driver resolution
|
||||
|
||||
## **[v5.0.0] - 09/01/2026**
|
||||
### 🎨 Styling
|
||||
|
||||
### Added
|
||||
- *(tests)* Fix spacing in docblock for allowedExtensions
|
||||
|
||||
- feat: ✨ Remove LDC Web API dependency
|
||||
## [1.0.2] - 2026-11-04
|
||||
|
||||
## **[v4.0.0] - 07/04/2025**
|
||||
### 🚀 Features
|
||||
|
||||
### Added
|
||||
- *(StorageServiceConfig)* Add prefix handling and validation
|
||||
|
||||
- feat: ✨ add Laravel 12 compatibility
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
## **[v3.0.0] - 03/09/2024**
|
||||
- *(cliff.toml)* Add spacing before release headings in changelog
|
||||
|
||||
### Added
|
||||
## [1.0.1] - 2026-11-04
|
||||
|
||||
- Add Laravel 11 compatibility
|
||||
- Bump deps for Laravel 11
|
||||
### 🚀 Features
|
||||
|
||||
### Changed
|
||||
- *(StorageFilePath)* Implement JsonSerializable interface
|
||||
|
||||
- Change namespace for UWIL complience
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
## **[v2.0.3] - 04/04/2024**
|
||||
- Update .gitignore and add bun.lock and cliff.toml
|
||||
- *(cliff.toml)* Skip changelog commits in parser
|
||||
|
||||
### Added
|
||||
## [1.0.0] - 2026-11-03
|
||||
|
||||
- Add staging to environnements where sync command should be executed
|
||||
### 🚀 Features
|
||||
|
||||
### Changed
|
||||
- *(Facades)* Add StorageService facade with methods for file operations
|
||||
- *(FileSanitizerImpl)* Add mimeType and size to SanitizedFile
|
||||
- *(StorageFilePath)* Implement Stringable interface
|
||||
- *(StorageService)* Add methods to store files using SplFileObject
|
||||
- *(MimeTypeChecker)* Add unsupported MIME types for SVG and XML
|
||||
- *(tests)* Add initial test structure and example tests
|
||||
|
||||
- PHPCS and PHPSTAN fixes
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
## **[v2.0.2] - 19/09/2024**
|
||||
- *(FileStream)* Improve stream validation in constructor
|
||||
- *(FileStream)* Handle resource creation failure in constructor
|
||||
|
||||
### Changed
|
||||
### 🚜 Refactor
|
||||
|
||||
- fixed artisan command to fetch list
|
||||
- *(DiskName)* Implement Stringable interface
|
||||
- *(StorageGatewayImpl)* Specify return type for getStorageForDisk
|
||||
- *(Exceptions)* Format exception classes for consistency
|
||||
- *(Utils, DiskName)* Improve disk handling with enum support
|
||||
- *(SanitizedFile)* Remove unused Utils methods
|
||||
- *(StorageService)* Change remove methods to return bool
|
||||
- *(FileStream)* Ensure resource is closed in destructor
|
||||
- *(exceptions)* Replace StorageException with UnexpectedStorageException
|
||||
- *(StorageServiceConfig)* Rename getEligiblesDisks to getEligibleDisks
|
||||
- *(StorageServiceProvider)* Use Arr::string for driver assignment
|
||||
- *(StorageServiceProvider)* Improve disk configuration handling
|
||||
- *(Utils)* Remove unused Facade import and redundant check
|
||||
|
||||
## **[v2.0.1] - 19/09/2023**
|
||||
### 🎨 Styling
|
||||
|
||||
### Changed
|
||||
- *(PathGenerators)* Format code for consistency
|
||||
- *(StorageService)* Disable line length check for facade comments
|
||||
- *(StorageService)* Update phpcs line length disable comment
|
||||
|
||||
- Delete API key
|
||||
- Artisan config cache fix
|
||||
- Cache was ignored
|
||||
### 🧪 Testing
|
||||
|
||||
## **[v2.0.0] - 05/06/2023**
|
||||
- Add unit tests for DiskName, FileStream, and SanitizedFile
|
||||
- *(DateHashPathGenerator)* Add unit tests for path generation
|
||||
- *(FileStream)* Skip test for chmod 0000 in superuser environments
|
||||
- *(StorageServiceConfig)* Add unit tests for configuration validation
|
||||
- *(FileSanitizerImpl)* Add tests for file sanitization errors
|
||||
- *(Utils)* Add unit tests for disk existence and extension validation
|
||||
- *(MimeTypeCheckerImpl)* Ignore type argument for PHPStan
|
||||
- *(FileSanitizerImpl)* Add PHPDoc for allowedExtensions parameter
|
||||
- *(MimeTypeCheckerImpl)* Ignore type argument for PHPStan
|
||||
- *(StorageGatewayImpl)* Add unit tests for file storage operations
|
||||
- *(StorageServiceImpl)* Add unit tests for file operations
|
||||
|
||||
### Added
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Bump laravel 10
|
||||
- Add manual Cird possibility
|
||||
- Remove FileStorageHelper class
|
||||
- Update PHP CodeSniffer and add Pint configuration
|
||||
- Add .gitattributes for line endings and export-ignore rules
|
||||
- Update phpcs and phpstan configurations
|
||||
- *(tests)* Add .gitkeep files for Feature and Integration directories
|
||||
- *(dependencies)* Update phpstan-mockery version in composer.json
|
||||
|
||||
### Changed
|
||||
|
||||
- Allows connections from private ip ranges
|
||||
- Add posibility to config manual CIDR exceptions
|
||||
|
||||
## **[v1.0.1] - 04/07/2022**
|
||||
|
||||
- Run scheduled only on production envirronement
|
||||
|
||||
## **[v1.0.0] - 24/06/2022**
|
||||
|
||||
- Initial release
|
||||
|
|
|
|||
73
bun.lock
Normal file
73
bun.lock
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"lockfileVersion": 2,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "storage-laravel",
|
||||
"devDependencies": {
|
||||
"git-cliff": "^2.14.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
|
||||
|
||||
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
||||
|
||||
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
|
||||
|
||||
"git-cliff": ["git-cliff@2.14.2", "", { "dependencies": { "execa": "^9.6.0" }, "optionalDependencies": { "git-cliff-darwin-arm64": "2.14.2", "git-cliff-darwin-x64": "2.14.2", "git-cliff-linux-arm64": "2.14.2", "git-cliff-linux-x64": "2.14.2", "git-cliff-windows-arm64": "2.14.2", "git-cliff-windows-x64": "2.14.2" }, "bin": "lib/cli/cli.js" }, "sha512-hlXIq4k2Tdj9C59WsD7FsLE0xRGFDozTUuoeqg9toMyVohaVxXiSiLnhM/IQyCmhOh5M4zuNckr2o8qMeuffbg=="],
|
||||
|
||||
"git-cliff-darwin-arm64": ["git-cliff-darwin-arm64@2.14.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Ylk39hv5flEUrXeoGvH6M3WjHBNERFH7a9DVEcGU2a8Q7a7fPtVasNk1fMuJ7CVbZCRIzUNyQV0ejRpxBdFnag=="],
|
||||
|
||||
"git-cliff-darwin-x64": ["git-cliff-darwin-x64@2.14.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-3FZNm0BGCIim/3G6uHSWAakrCaGmC47lcI+Cob9CZPMBYHKrenYk0i6B8MlObSXr2Lr7Ve1BpGbd/foQg4NOiA=="],
|
||||
|
||||
"git-cliff-linux-arm64": ["git-cliff-linux-arm64@2.14.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3oMDpoyfsy2angD9ulcCBtyWCkzZqN3spmmtb0jic8OKAu1p+CALS8oep7J8AdgkDojuLo6bdHiSZIM9PeTxmQ=="],
|
||||
|
||||
"git-cliff-linux-x64": ["git-cliff-linux-x64@2.14.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4EaBYwBiCupECZ2rzC6CelNSGYvkvtJeja4wXLDGtfKnRhCN/WNoqbM+hvrtm32uf5qovfoNoDA906F8itw+oQ=="],
|
||||
|
||||
"git-cliff-windows-arm64": ["git-cliff-windows-arm64@2.14.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-0oc0xD/6A+BvInz8imBJulXVOlR9M0nL+d3ibL0lwJvxUVTMQzvoCnsn9OxpsdzXrEisnTvaf+GExoKVJkUcDw=="],
|
||||
|
||||
"git-cliff-windows-x64": ["git-cliff-windows-x64@2.14.2", "", { "os": "win32", "cpu": "x64" }, "sha512-RFmCMiwGMU8O+rF//wxT7L10ssoDtzI+QFxlbjpA8E1BAEafkmLTQkKalT88ipEweD46P+Sfc20v8RXuE7V/UA=="],
|
||||
|
||||
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
|
||||
|
||||
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"pretty-ms": ["pretty-ms@9.3.1", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
}
|
||||
}
|
||||
102
cliff.toml
Normal file
102
cliff.toml
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
|
||||
[changelog]
|
||||
header = """
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
- This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
- Commits respect [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) & use [Gitmoji](https://gitmoji.dev/).
|
||||
"""
|
||||
# Used by `--prepend` to remove the previous header.
|
||||
header_marker = "<!-- git-cliff: end of header -->"
|
||||
# A Tera template to be rendered for each release in the changelog.
|
||||
# See https://keats.github.io/tera/#getting-started
|
||||
body = """\
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for entry in commits | commit_groups(groups=commit_parsers_groups) %}
|
||||
### {{ entry.group | striptags | trim | upper_first }}
|
||||
{% for commit in entry.commits %}
|
||||
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
||||
{% if commit.breaking %}[**breaking**] {% endif %}\
|
||||
{{ commit.message | upper_first }}\
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
"""
|
||||
# Remove leading and trailing whitespaces from the changelog's body.
|
||||
trim = false
|
||||
# Render body even when there are no releases to process.
|
||||
render_always = true
|
||||
postprocessors = []
|
||||
# render body even when there are no releases to process
|
||||
# render_always = true
|
||||
# output file path
|
||||
# output = "test.md"
|
||||
|
||||
[git]
|
||||
# Parse commits according to the conventional commits specification.
|
||||
# See https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# Exclude commits that do not match the conventional commits specification.
|
||||
filter_unconventional = true
|
||||
# Require all commits to be conventional.
|
||||
# Takes precedence over filter_unconventional.
|
||||
require_conventional = false
|
||||
# Split commits on newlines, treating each line as an individual commit.
|
||||
split_commits = false
|
||||
# An array of regex based parsers to modify commit messages prior to further processing.
|
||||
commit_preprocessors = [
|
||||
# Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
|
||||
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
|
||||
# Check spelling of the commit message using https://github.com/crate-ci/typos.
|
||||
# If the spelling is incorrect, it will be fixed automatically.
|
||||
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
|
||||
]
|
||||
# Prevent commits that are breaking from being excluded by commit parsers.
|
||||
protect_breaking_commits = false
|
||||
# An array of regex based parsers for extracting data from the commit message.
|
||||
# Assigns commits to groups.
|
||||
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
||||
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
||||
{ message = "^doc", skip = true },
|
||||
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(deps.*\\)", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore\\(changelog\\)", skip = true },
|
||||
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
||||
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
||||
{ message = ".*", group = "<!-- 10 -->💼 Other" },
|
||||
]
|
||||
# Exclude commits that are not matched by any commit parser.
|
||||
filter_commits = false
|
||||
# Fail on a commit that is not matched by any commit parser.
|
||||
fail_on_unmatched_commit = false
|
||||
# An array of link parsers for extracting external references, and turning them into URLs, using regex.
|
||||
link_parsers = []
|
||||
# Include only the tags that belong to the current branch.
|
||||
use_branch_tags = false
|
||||
# Order releases topologically instead of chronologically.
|
||||
topo_order = false
|
||||
# Order commits topologically instead of chronologically.
|
||||
topo_order_commits = true
|
||||
# Order of commits in each group/release within the changelog.
|
||||
# Allowed values: newest, oldest
|
||||
sort_commits = "oldest"
|
||||
# Process submodules commits
|
||||
recurse_submodules = false
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "uwil/storage-laravel",
|
||||
"description": "Handles file storage",
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.3",
|
||||
"readme": "https://lab.s-web.fr/ldc-web/projets/storage-laravel/-/blob/main/README.md",
|
||||
"support": {
|
||||
"email": "digital@ldc.fr",
|
||||
|
|
|
|||
|
|
@ -9,5 +9,7 @@ return [
|
|||
'local',
|
||||
],
|
||||
|
||||
'prefix' => env('STORAGE_SERVICE_PREFIX', 'media'),
|
||||
|
||||
'allowedExtensions' => ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'txt', 'doc', 'docx', 'csv', 'mp4', 'webm'],
|
||||
];
|
||||
|
|
|
|||
12
package.json
Normal file
12
package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "file-storage-service",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"changelog": "bunx git-cliff -o CHANGELOG.md",
|
||||
"changelog:conventional": "bunx conventional-changelog-cli -p conventionalcommits -i CHANGELOG.md -s -r 0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"git-cliff": "^2.14.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,11 @@ final class StorageServiceConfig
|
|||
/** @var DiskName[] */
|
||||
private array $eligibles;
|
||||
|
||||
private ?string $prefix = null;
|
||||
|
||||
/** @var array<string, string> Laravel disk name => physical driver (scoped disks resolved to their parent) */
|
||||
private array $realDrivers = [];
|
||||
|
||||
/**
|
||||
* @param string[] $eligibleDisks
|
||||
* @param object{'disk':string,'driver':string}[] $laravelExistingDisks
|
||||
|
|
@ -26,8 +31,61 @@ final class StorageServiceConfig
|
|||
private string $defaultDisk,
|
||||
private array $eligibleDisks,
|
||||
private array $laravelExistingDisks,
|
||||
private array $allowedExtensions
|
||||
private array $allowedExtensions,
|
||||
?string $prefix = null
|
||||
) {
|
||||
$this->validateAndSetDisks();
|
||||
$this->validateExtensions();
|
||||
$this->prefix = $this->parsePrefix($prefix);
|
||||
}
|
||||
|
||||
private function parsePrefix(?string $prefix): ?string
|
||||
{
|
||||
if ($prefix === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$trimmed = mb_trim($prefix);
|
||||
$clean = trim($trimmed, '/');
|
||||
|
||||
if ($clean === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_contains($clean, '..')) {
|
||||
throw new UnexpectedStorageException('Prefix must not contain "..".');
|
||||
}
|
||||
|
||||
if (str_contains($clean, '\\')) {
|
||||
throw new UnexpectedStorageException('Prefix must not contain "\\".');
|
||||
}
|
||||
|
||||
if (str_contains($clean, '//')) {
|
||||
throw new UnexpectedStorageException('Prefix must not contain "//".');
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
private function validateExtensions(): void
|
||||
{
|
||||
if (empty($this->allowedExtensions)) {
|
||||
throw new UnexpectedStorageException('Allowed extensions cannot be empty');
|
||||
}
|
||||
|
||||
foreach ($this->allowedExtensions as $extension) {
|
||||
if (! is_string($extension) || empty(mb_trim($extension))) {
|
||||
throw new UnexpectedStorageException("Allowed extension {$extension} is not valid");
|
||||
}
|
||||
|
||||
if (in_array(strtolower(ltrim($extension, '.')), self::UNSUPPORTED_EXTENSIONS, true)) {
|
||||
throw new UnexpectedStorageException("Extension '{$extension}' is forbidden: XML/SVG sanitization is currently not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function validateAndSetDisks(): void
|
||||
{
|
||||
if (Utils::diskExists($this->defaultDisk) === false) {
|
||||
throw new UnexpectedStorageException('Default disk does not exist');
|
||||
}
|
||||
|
|
@ -86,21 +144,21 @@ final class StorageServiceConfig
|
|||
$disk->driver
|
||||
), $intersectWithDrivers);
|
||||
|
||||
if (empty($this->allowedExtensions)) {
|
||||
throw new UnexpectedStorageException('Allowed extensions cannot be empty');
|
||||
}
|
||||
$this->resolveRealDrivers();
|
||||
}
|
||||
|
||||
foreach ($this->allowedExtensions as $extension) {
|
||||
if (! is_string($extension) || empty(mb_trim($extension))) {
|
||||
throw new UnexpectedStorageException("Allowed extension {$extension} is not valid");
|
||||
}
|
||||
|
||||
if (in_array(strtolower(ltrim($extension, '.')), self::UNSUPPORTED_EXTENSIONS, true)) {
|
||||
throw new UnexpectedStorageException("Extension '{$extension}' is forbidden: XML/SVG sanitization is currently not supported.");
|
||||
}
|
||||
private function resolveRealDrivers(): void
|
||||
{
|
||||
foreach ([$this->default, ...$this->eligibles] as $disk) {
|
||||
$this->realDrivers[$disk->value] = Utils::resolveDriver($disk->value, $disk->driver);
|
||||
}
|
||||
}
|
||||
|
||||
public function getPrefix(): ?string
|
||||
{
|
||||
return $this->prefix;
|
||||
}
|
||||
|
||||
public function getDefaultDisk(): DiskName
|
||||
{
|
||||
return $this->default;
|
||||
|
|
@ -145,4 +203,15 @@ final class StorageServiceConfig
|
|||
{
|
||||
return $this->allowedExtensions;
|
||||
}
|
||||
|
||||
public function getRealDriver(DiskName $disk): string
|
||||
{
|
||||
return $this->realDrivers[$disk->value]
|
||||
?? throw new UnexpectedStorageException("Disk {$disk->value} is neither the default nor an eligible disk");
|
||||
}
|
||||
|
||||
public function isS3Driver(DiskName $disk): bool
|
||||
{
|
||||
return $this->getRealDriver($disk) === 's3';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Uwil\Storage\Lib\PathGenerators;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Uwil\Storage\Config\StorageServiceConfig;
|
||||
use Uwil\Storage\Contracts\PathGenerator;
|
||||
use Uwil\Storage\Lib\ValueObjects\DiskName;
|
||||
use Uwil\Storage\Lib\ValueObjects\SanitizedFile;
|
||||
|
|
@ -12,8 +13,13 @@ use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
|||
|
||||
class DateHashPathGenerator implements PathGenerator
|
||||
{
|
||||
public function __construct(
|
||||
private ?StorageServiceConfig $config = null
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function generate(DiskName $disk, SanitizedFile $file, ?string $directory = null): StorageFilePath
|
||||
public function generate(DiskName $disk, SanitizedFile $file): StorageFilePath
|
||||
{
|
||||
$extension = pathinfo($file->sanitizedName, PATHINFO_EXTENSION);
|
||||
$filename = pathinfo($file->sanitizedName, PATHINFO_FILENAME);
|
||||
|
|
@ -30,7 +36,7 @@ class DateHashPathGenerator implements PathGenerator
|
|||
);
|
||||
|
||||
$pathSegments = array_filter([
|
||||
$directory ? trim($directory, '/') : null,
|
||||
$this->config?->getPrefix(),
|
||||
gmdate('Y'),
|
||||
gmdate('m'),
|
||||
gmdate('d'),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Uwil\Storage\Lib\PathGenerators;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Uwil\Storage\Config\StorageServiceConfig;
|
||||
use Uwil\Storage\Contracts\PathGenerator;
|
||||
use Uwil\Storage\Lib\ValueObjects\DiskName;
|
||||
use Uwil\Storage\Lib\ValueObjects\SanitizedFile;
|
||||
|
|
@ -12,8 +13,13 @@ use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
|||
|
||||
class UuidPathGenerator implements PathGenerator
|
||||
{
|
||||
public function __construct(
|
||||
private ?StorageServiceConfig $config = null
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function generate(DiskName $disk, SanitizedFile $file, ?string $directory = null): StorageFilePath
|
||||
public function generate(DiskName $disk, SanitizedFile $file): StorageFilePath
|
||||
{
|
||||
$extension = pathinfo($file->sanitizedName, PATHINFO_EXTENSION);
|
||||
$uuid = Str::uuid()->toString();
|
||||
|
|
@ -21,7 +27,7 @@ class UuidPathGenerator implements PathGenerator
|
|||
$finalFilename = $uuid . (! empty($extension) ? '.' . $extension : '');
|
||||
|
||||
$pathSegments = array_filter([
|
||||
$directory ? trim($directory, '/') : null,
|
||||
$this->config?->getPrefix(),
|
||||
$finalFilename,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ namespace Uwil\Storage\Lib;
|
|||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Uwil\Storage\Config\StorageServiceConfig;
|
||||
use Uwil\Storage\Contracts\PathGenerator;
|
||||
use Uwil\Storage\Contracts\StorageGateway;
|
||||
use Uwil\Storage\Exceptions\StorageException;
|
||||
|
|
@ -21,6 +22,7 @@ use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
|||
readonly class StorageGatewayImpl implements StorageGateway
|
||||
{
|
||||
public function __construct(
|
||||
private StorageServiceConfig $config,
|
||||
private LoggerInterface $logger,
|
||||
private DateHashPathGenerator $datePathGenerator,
|
||||
private UuidPathGenerator $uuidPathGenerator
|
||||
|
|
@ -123,12 +125,12 @@ readonly class StorageGatewayImpl implements StorageGateway
|
|||
|
||||
private function pathGeneratorFor(DiskName $disk): PathGenerator
|
||||
{
|
||||
return match ($disk->driver) {
|
||||
'local' => $this->datePathGenerator,
|
||||
'ftp' => $this->datePathGenerator,
|
||||
'sftp' => $this->datePathGenerator,
|
||||
's3' => $this->uuidPathGenerator,
|
||||
default => throw new UnexpectedStorageException("Unsupported disk driver: {$disk->driver} on disk {$disk->value}"),
|
||||
$realDriver = $this->config->getRealDriver($disk);
|
||||
|
||||
return match ($realDriver) {
|
||||
'local', 'ftp', 'sftp' => $this->datePathGenerator,
|
||||
's3' => $this->uuidPathGenerator,
|
||||
default => throw new UnexpectedStorageException("Unsupported disk driver: {$realDriver} on disk {$disk->value}"),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\Storage\Lib;
|
||||
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Uwil\Storage\Config\StorageServiceConfig;
|
||||
use Uwil\Storage\Exceptions\UnexpectedStorageException;
|
||||
|
||||
class Utils
|
||||
{
|
||||
|
|
@ -14,6 +16,45 @@ class Utils
|
|||
// Abstract class.
|
||||
}
|
||||
|
||||
public static function resolveDriver(string $disk, ?string $driver = null): string
|
||||
{
|
||||
$currentDisk = $disk;
|
||||
|
||||
if ($driver === null) {
|
||||
/** @var mixed $raw */
|
||||
$raw = Config::get("filesystems.disks.{$currentDisk}.driver");
|
||||
$driver = is_string($raw) ? $raw : '';
|
||||
}
|
||||
|
||||
$visited = [$currentDisk];
|
||||
|
||||
while ($driver === 'scoped') {
|
||||
/** @var mixed $parentDisk */
|
||||
$parentDisk = Config::get("filesystems.disks.{$currentDisk}.disk");
|
||||
|
||||
if (! is_string($parentDisk) || $parentDisk === '') {
|
||||
throw new UnexpectedStorageException("Scoped disk '{$currentDisk}' is missing a valid parent 'disk' configuration.");
|
||||
}
|
||||
|
||||
if (in_array($parentDisk, $visited, true)) {
|
||||
throw new UnexpectedStorageException("Circular reference detected in scoped disk '{$currentDisk}'.");
|
||||
}
|
||||
|
||||
/** @var mixed $parentDriver */
|
||||
$parentDriver = Config::get("filesystems.disks.{$parentDisk}.driver");
|
||||
|
||||
if (! is_string($parentDriver) || $parentDriver === '') {
|
||||
throw new UnexpectedStorageException("Parent disk '{$parentDisk}' of scoped disk '{$currentDisk}' has no valid driver configured.");
|
||||
}
|
||||
|
||||
$visited[] = $parentDisk;
|
||||
$driver = $parentDriver;
|
||||
$currentDisk = $parentDisk;
|
||||
}
|
||||
|
||||
return $driver;
|
||||
}
|
||||
|
||||
public static function diskExists(string | \UnitEnum $disk): bool
|
||||
{
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use Uwil\Storage\Lib\Utils;
|
|||
|
||||
final readonly class DiskName implements \Stringable
|
||||
{
|
||||
public const array ALLOWED_DRIVERS = ['local', 'ftp', 'sftp', 's3'];
|
||||
public const array ALLOWED_DRIVERS = ['local', 'ftp', 'sftp', 's3', 'scoped'];
|
||||
|
||||
public string $value;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ namespace Uwil\Storage\Lib\ValueObjects;
|
|||
|
||||
use Uwil\Storage\Exceptions\UnexpectedStorageException;
|
||||
|
||||
final readonly class StorageFilePath implements \Stringable
|
||||
final readonly class StorageFilePath implements \JsonSerializable, \Stringable
|
||||
{
|
||||
public function __construct(
|
||||
public string $value
|
||||
|
|
@ -36,6 +36,11 @@ final readonly class StorageFilePath implements \Stringable
|
|||
}
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ class StorageServiceProvider extends ServiceProvider
|
|||
fn ($ext) => is_string($ext)
|
||||
);
|
||||
|
||||
$rawPrefix = Config::get('storage-service.prefix');
|
||||
$prefix = is_string($rawPrefix) ? $rawPrefix : null;
|
||||
|
||||
$disks = [];
|
||||
foreach ($rawDisks as $name => $diskConf) {
|
||||
$disks[] = (object) [
|
||||
|
|
@ -63,7 +66,8 @@ class StorageServiceProvider extends ServiceProvider
|
|||
Config::string('storage-service.default', 'local'),
|
||||
$eligibleDisks,
|
||||
$disks,
|
||||
$allowedExtensions
|
||||
$allowedExtensions,
|
||||
$prefix
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -71,9 +75,24 @@ class StorageServiceProvider extends ServiceProvider
|
|||
$this->app->bind(MimeTypeChecker::class, MimeTypeCheckerImpl::class);
|
||||
$this->app->bind(FileSanitizer::class, FileSanitizerImpl::class);
|
||||
|
||||
$this->app->bind(
|
||||
DateHashPathGenerator::class,
|
||||
fn (Application $app) => new DateHashPathGenerator(
|
||||
$app->make(StorageServiceConfig::class)
|
||||
)
|
||||
);
|
||||
|
||||
$this->app->bind(
|
||||
UuidPathGenerator::class,
|
||||
fn (Application $app) => new UuidPathGenerator(
|
||||
$app->make(StorageServiceConfig::class)
|
||||
)
|
||||
);
|
||||
|
||||
$this->app->bind(
|
||||
StorageGateway::class,
|
||||
fn (Application $app) => new StorageGatewayImpl(
|
||||
$app->make(StorageServiceConfig::class),
|
||||
$app->make(LoggerInterface::class),
|
||||
$app->make(DateHashPathGenerator::class),
|
||||
$app->make(UuidPathGenerator::class),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\Storage\Tests\Unit\Config;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Uwil\Storage\Config\StorageServiceConfig;
|
||||
use Uwil\Storage\Exceptions\UnexpectedStorageException;
|
||||
use Uwil\Storage\Lib\ValueObjects\DiskName;
|
||||
|
|
@ -354,3 +355,158 @@ describe('StorageServiceConfig isExtensionAllowed', function (): void {
|
|||
'.svg' => '.svg',
|
||||
]);
|
||||
});
|
||||
|
||||
describe('StorageServiceConfig prefix validation and retrieval', function (): void {
|
||||
it('returns null when prefix is omitted or null', function (?string $prefix): void {
|
||||
// GIVEN
|
||||
$config = new StorageServiceConfig(
|
||||
defaultDisk: 'local',
|
||||
eligibleDisks: ['local'],
|
||||
laravelExistingDisks: [(object) ['disk' => 'local', 'driver' => 'local']],
|
||||
allowedExtensions: ['pdf'],
|
||||
prefix: $prefix
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($config->getPrefix())->toBeNull();
|
||||
})->with([
|
||||
'null' => null,
|
||||
'empty' => '',
|
||||
'spaces' => ' ',
|
||||
'slashes' => '///',
|
||||
]);
|
||||
|
||||
it('trims leading and trailing slashes and spaces from prefix', function (string $input, string $expected): void {
|
||||
// GIVEN
|
||||
$config = new StorageServiceConfig(
|
||||
defaultDisk: 'local',
|
||||
eligibleDisks: ['local'],
|
||||
laravelExistingDisks: [(object) ['disk' => 'local', 'driver' => 'local']],
|
||||
allowedExtensions: ['pdf'],
|
||||
prefix: $input
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($config->getPrefix())->toBe($expected);
|
||||
})->with([
|
||||
'simple' => ['media', 'media'],
|
||||
'leading slash' => ['/media', 'media'],
|
||||
'trailing slash' => ['media/', 'media'],
|
||||
'both slashes' => ['/media/', 'media'],
|
||||
'nested' => ['/uploads/images/', 'uploads/images'],
|
||||
'with spaces' => [' media ', 'media'],
|
||||
]);
|
||||
|
||||
it('throws exception if prefix contains invalid sequences', function (string $invalidPrefix): void {
|
||||
// GIVEN
|
||||
$action = fn () => new StorageServiceConfig(
|
||||
defaultDisk: 'local',
|
||||
eligibleDisks: ['local'],
|
||||
laravelExistingDisks: [(object) ['disk' => 'local', 'driver' => 'local']],
|
||||
allowedExtensions: ['pdf'],
|
||||
prefix: $invalidPrefix
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(UnexpectedStorageException::class);
|
||||
})->with([
|
||||
'dot dot' => '../media',
|
||||
'nested dot dot' => 'media/../sub',
|
||||
'backslash' => 'media\\sub',
|
||||
'double slash' => 'media//sub',
|
||||
]);
|
||||
});
|
||||
|
||||
describe('StorageServiceConfig real driver resolution', function (): void {
|
||||
it('resolves direct drivers of default and eligible disks', function (): void {
|
||||
// GIVEN
|
||||
Storage::fake('s3');
|
||||
|
||||
// WHEN
|
||||
$config = new StorageServiceConfig(
|
||||
defaultDisk: 'local',
|
||||
eligibleDisks: ['local', 's3'],
|
||||
laravelExistingDisks: [
|
||||
(object) ['disk' => 'local', 'driver' => 'local'],
|
||||
(object) ['disk' => 's3', 'driver' => 's3'],
|
||||
],
|
||||
allowedExtensions: ['pdf']
|
||||
);
|
||||
$localDisk = new DiskName('local', 'local');
|
||||
$s3Disk = new DiskName('s3', 's3');
|
||||
|
||||
// THEN
|
||||
expect($config->getRealDriver($localDisk))->toBe('local')
|
||||
->and($config->isS3Driver($localDisk))->toBeFalse()
|
||||
->and($config->getRealDriver($s3Disk))->toBe('s3')
|
||||
->and($config->isS3Driver($s3Disk))->toBeTrue();
|
||||
});
|
||||
|
||||
it('resolves the underlying driver of a scoped disk at boot', function (): void {
|
||||
// GIVEN
|
||||
config()->set('filesystems.disks.s3_parent', ['driver' => 's3']);
|
||||
config()->set('filesystems.disks.media', [
|
||||
'driver' => 'scoped',
|
||||
'disk' => 's3_parent',
|
||||
'prefix' => 'media',
|
||||
]);
|
||||
Storage::fake('media');
|
||||
|
||||
// WHEN
|
||||
$config = new StorageServiceConfig(
|
||||
defaultDisk: 'media',
|
||||
eligibleDisks: ['media'],
|
||||
laravelExistingDisks: [(object) ['disk' => 'media', 'driver' => 'scoped']],
|
||||
allowedExtensions: ['pdf']
|
||||
);
|
||||
$media = new DiskName('media', 'scoped');
|
||||
|
||||
// THEN
|
||||
expect($config->getRealDriver($media))->toBe('s3')
|
||||
->and($config->isS3Driver($media))->toBeTrue();
|
||||
});
|
||||
|
||||
it('fails at boot when a scoped disk is misconfigured', function (): void {
|
||||
// GIVEN
|
||||
config()->set('filesystems.disks.broken', ['driver' => 'scoped']);
|
||||
Storage::fake('broken');
|
||||
|
||||
// WHEN
|
||||
$action = fn () => new StorageServiceConfig(
|
||||
defaultDisk: 'local',
|
||||
eligibleDisks: ['local', 'broken'],
|
||||
laravelExistingDisks: [
|
||||
(object) ['disk' => 'local', 'driver' => 'local'],
|
||||
(object) ['disk' => 'broken', 'driver' => 'scoped'],
|
||||
],
|
||||
allowedExtensions: ['pdf']
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(
|
||||
UnexpectedStorageException::class,
|
||||
"Scoped disk 'broken' is missing a valid parent 'disk' configuration."
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when asking the driver of a disk that is neither default nor eligible', function (): void {
|
||||
// GIVEN
|
||||
$config = new StorageServiceConfig(
|
||||
defaultDisk: 'local',
|
||||
eligibleDisks: ['local'],
|
||||
laravelExistingDisks: [(object) ['disk' => 'local', 'driver' => 'local']],
|
||||
allowedExtensions: ['pdf']
|
||||
);
|
||||
Storage::fake('other');
|
||||
$other = new DiskName('other', 'local');
|
||||
|
||||
// WHEN
|
||||
$action = fn () => $config->getRealDriver($other);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(
|
||||
UnexpectedStorageException::class,
|
||||
'Disk other is neither the default nor an eligible disk'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use Uwil\Storage\Lib\ValueObjects\SanitizedFile;
|
|||
covers(FileSanitizerImpl::class);
|
||||
|
||||
/**
|
||||
* @param array<string> $allowedExtensions
|
||||
* @param array<string> $allowedExtensions
|
||||
*/
|
||||
function createConfig(array $allowedExtensions = ['jpg', 'png', 'pdf', 'txt']): StorageServiceConfig
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\Storage\Tests\Unit\Lib\PathGenerators;
|
||||
|
||||
use Uwil\Storage\Config\StorageServiceConfig;
|
||||
use Uwil\Storage\Lib\PathGenerators\DateHashPathGenerator;
|
||||
use Uwil\Storage\Lib\ValueObjects\DiskName;
|
||||
use Uwil\Storage\Lib\ValueObjects\FileStream;
|
||||
|
|
@ -91,20 +92,26 @@ describe('DateHashPathGenerator generation without directory', function (): void
|
|||
});
|
||||
});
|
||||
|
||||
describe('DateHashPathGenerator generation with directory', function (): void {
|
||||
it('prepends the given directory to the path', function (): void {
|
||||
describe('DateHashPathGenerator generation with prefix', function (): void {
|
||||
it('prepends the configured prefix to the path', function (): void {
|
||||
// GIVEN
|
||||
$generator = new DateHashPathGenerator();
|
||||
$config = new StorageServiceConfig(
|
||||
defaultDisk: 'local',
|
||||
eligibleDisks: ['local'],
|
||||
laravelExistingDisks: [(object) ['disk' => 'local', 'driver' => 'local']],
|
||||
allowedExtensions: ['png'],
|
||||
prefix: 'avatars/users'
|
||||
);
|
||||
$generator = new DateHashPathGenerator($config);
|
||||
$disk = new DiskName('local', 'local');
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'avatar.png',
|
||||
sanitizedName: 'avatar.png',
|
||||
contents: FileStream::fromString('image data')
|
||||
);
|
||||
$directory = 'avatars/users';
|
||||
|
||||
// WHEN
|
||||
$result = $generator->generate($disk, $sanitizedFile, $directory);
|
||||
$result = $generator->generate($disk, $sanitizedFile);
|
||||
|
||||
// THEN
|
||||
$expectedHash = substr(hash('sha256', 'avatar.png'), 0, 2);
|
||||
|
|
@ -117,49 +124,6 @@ describe('DateHashPathGenerator generation with directory', function (): void {
|
|||
);
|
||||
expect($result->value)->toMatch($pattern);
|
||||
});
|
||||
|
||||
it('trims leading and trailing slashes from the directory', function (string $directory): void {
|
||||
// GIVEN
|
||||
$generator = new DateHashPathGenerator();
|
||||
$disk = new DiskName('local', 'local');
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'file.txt',
|
||||
sanitizedName: 'file.txt',
|
||||
contents: FileStream::fromString('txt')
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$result = $generator->generate($disk, $sanitizedFile, $directory);
|
||||
|
||||
// THEN
|
||||
expect($result->value)->toStartWith('uploads/archive/');
|
||||
})->with([
|
||||
'leading slash' => '/uploads/archive',
|
||||
'trailing slash' => 'uploads/archive/',
|
||||
'both slashes' => '/uploads/archive/',
|
||||
'multiple outer slashes' => '///uploads/archive///',
|
||||
]);
|
||||
|
||||
it('ignores empty or slash-only directory', function (string $directory): void {
|
||||
// GIVEN
|
||||
$generator = new DateHashPathGenerator();
|
||||
$disk = new DiskName('local', 'local');
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'report.pdf',
|
||||
sanitizedName: 'report.pdf',
|
||||
contents: FileStream::fromString('pdf')
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$result = $generator->generate($disk, $sanitizedFile, $directory);
|
||||
|
||||
// THEN
|
||||
expect($result->value)->toStartWith(gmdate('Y') . '/');
|
||||
})->with([
|
||||
'empty string' => '',
|
||||
'single slash' => '/',
|
||||
'multi slashes' => '///',
|
||||
]);
|
||||
});
|
||||
|
||||
describe('DateHashPathGenerator filename slugging', function (): void {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Uwil\Storage\Tests\Unit\Lib\PathGenerators;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Uwil\Storage\Config\StorageServiceConfig;
|
||||
use Uwil\Storage\Lib\PathGenerators\UuidPathGenerator;
|
||||
use Uwil\Storage\Lib\ValueObjects\DiskName;
|
||||
use Uwil\Storage\Lib\ValueObjects\FileStream;
|
||||
|
|
@ -74,67 +75,30 @@ describe('UuidPathGenerator generation without directory', function (): void {
|
|||
});
|
||||
});
|
||||
|
||||
describe('UuidPathGenerator generation with directory', function (): void {
|
||||
it('prepends the given directory to the generated UUID filename', function (): void {
|
||||
describe('UuidPathGenerator generation with prefix', function (): void {
|
||||
it('prepends the configured prefix to the generated UUID filename', function (): void {
|
||||
// GIVEN
|
||||
$generator = new UuidPathGenerator();
|
||||
$config = new StorageServiceConfig(
|
||||
defaultDisk: 'local',
|
||||
eligibleDisks: ['local'],
|
||||
laravelExistingDisks: [(object) ['disk' => 'local', 'driver' => 'local']],
|
||||
allowedExtensions: ['pdf'],
|
||||
prefix: 'billing/invoices'
|
||||
);
|
||||
$generator = new UuidPathGenerator($config);
|
||||
$disk = new DiskName('local', 'local');
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'invoice.pdf',
|
||||
sanitizedName: 'invoice.pdf',
|
||||
contents: FileStream::fromString('PDF DATA')
|
||||
);
|
||||
$directory = 'billing/invoices';
|
||||
|
||||
// WHEN
|
||||
$result = $generator->generate($disk, $sanitizedFile, $directory);
|
||||
$result = $generator->generate($disk, $sanitizedFile);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(StorageFilePath::class)
|
||||
->and($result->value)->toStartWith('billing/invoices/')
|
||||
->and($result->value)->toMatch('#^billing/invoices/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.pdf$#');
|
||||
});
|
||||
|
||||
it('trims leading and trailing slashes from the directory', function (string $directory): void {
|
||||
// GIVEN
|
||||
$generator = new UuidPathGenerator();
|
||||
$disk = new DiskName('local', 'local');
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'file.txt',
|
||||
sanitizedName: 'file.txt',
|
||||
contents: FileStream::fromString('text')
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$result = $generator->generate($disk, $sanitizedFile, $directory);
|
||||
|
||||
// THEN
|
||||
expect($result->value)->toStartWith('custom/path/');
|
||||
})->with([
|
||||
'leading slash' => '/custom/path',
|
||||
'trailing slash' => 'custom/path/',
|
||||
'both slashes' => '/custom/path/',
|
||||
'multiple outer slashes' => '///custom/path///',
|
||||
]);
|
||||
|
||||
it('ignores empty or slash-only directory', function (string $directory): void {
|
||||
// GIVEN
|
||||
$generator = new UuidPathGenerator();
|
||||
$disk = new DiskName('local', 'local');
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'file.txt',
|
||||
sanitizedName: 'file.txt',
|
||||
contents: FileStream::fromString('text')
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$result = $generator->generate($disk, $sanitizedFile, $directory);
|
||||
|
||||
// THEN
|
||||
expect($result->value)->toMatch('#^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.txt$#');
|
||||
})->with([
|
||||
'empty string' => '',
|
||||
'single slash' => '/',
|
||||
'multi slashes' => '///',
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\Storage\Tests\Unit\Lib;
|
||||
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Uwil\Storage\Config\StorageServiceConfig;
|
||||
use Uwil\Storage\Exceptions\UnexpectedStorageException;
|
||||
use Uwil\Storage\Lib\PathGenerators\DateHashPathGenerator;
|
||||
use Uwil\Storage\Lib\PathGenerators\UuidPathGenerator;
|
||||
|
|
@ -14,14 +17,36 @@ use Uwil\Storage\Lib\ValueObjects\DiskName;
|
|||
use Uwil\Storage\Lib\ValueObjects\FileStream;
|
||||
use Uwil\Storage\Lib\ValueObjects\SanitizedFile;
|
||||
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
covers(StorageGatewayImpl::class);
|
||||
|
||||
function createGateway(): StorageGatewayImpl
|
||||
/**
|
||||
* @param array<string, string> $extraDisks eligible disks to declare in addition to `local` (name => driver)
|
||||
*/
|
||||
function createGatewayConfig(array $extraDisks = []): StorageServiceConfig
|
||||
{
|
||||
$existing = [(object) ['disk' => 'local', 'driver' => 'local']];
|
||||
|
||||
foreach ($extraDisks as $name => $driver) {
|
||||
Storage::fake($name);
|
||||
$existing[] = (object) ['disk' => $name, 'driver' => $driver];
|
||||
}
|
||||
|
||||
return new StorageServiceConfig(
|
||||
defaultDisk: 'local',
|
||||
eligibleDisks: ['local', ...array_keys($extraDisks)],
|
||||
laravelExistingDisks: $existing,
|
||||
allowedExtensions: ['txt']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $extraDisks
|
||||
*/
|
||||
function createGateway(array $extraDisks = []): StorageGatewayImpl
|
||||
{
|
||||
return new StorageGatewayImpl(
|
||||
config: createGatewayConfig($extraDisks),
|
||||
logger: new NullLogger(),
|
||||
datePathGenerator: new DateHashPathGenerator(),
|
||||
uuidPathGenerator: new UuidPathGenerator()
|
||||
|
|
@ -52,7 +77,7 @@ describe('StorageGatewayImpl storeFile', function (): void {
|
|||
it('stores a file onto an s3 disk using UuidPathGenerator', function (): void {
|
||||
// GIVEN
|
||||
Storage::fake('s3');
|
||||
$gateway = createGateway();
|
||||
$gateway = createGateway(['s3' => 's3']);
|
||||
$disk = new DiskName('s3', 's3');
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'picture.png',
|
||||
|
|
@ -74,9 +99,9 @@ describe('StorageGatewayImpl storeFile', function (): void {
|
|||
$mockStorage = \Mockery::mock(Filesystem::class);
|
||||
$mockStorage->shouldReceive('put')->andReturn(false);
|
||||
|
||||
$gateway = createGateway();
|
||||
Storage::shouldReceive('disk')->with('local')->andReturn($mockStorage);
|
||||
|
||||
$gateway = createGateway();
|
||||
$disk = new DiskName('local', 'local');
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'test.txt',
|
||||
|
|
@ -96,9 +121,9 @@ describe('StorageGatewayImpl storeFile', function (): void {
|
|||
$mockStorage = \Mockery::mock(Filesystem::class);
|
||||
$mockStorage->shouldReceive('put')->andThrow(new \RuntimeException('Network drop'));
|
||||
|
||||
$gateway = createGateway();
|
||||
Storage::shouldReceive('disk')->with('local')->andReturn($mockStorage);
|
||||
|
||||
$gateway = createGateway();
|
||||
$disk = new DiskName('local', 'local');
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'test.txt',
|
||||
|
|
@ -119,9 +144,9 @@ describe('StorageGatewayImpl storeFile', function (): void {
|
|||
$mockStorage = \Mockery::mock(Filesystem::class);
|
||||
$mockStorage->shouldReceive('put')->andThrow($directException);
|
||||
|
||||
$gateway = createGateway();
|
||||
Storage::shouldReceive('disk')->with('local')->andReturn($mockStorage);
|
||||
|
||||
$gateway = createGateway();
|
||||
$disk = new DiskName('local', 'local');
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'test.txt',
|
||||
|
|
@ -143,9 +168,9 @@ describe('StorageGatewayImpl storeFile', function (): void {
|
|||
config()->set("filesystems.disks.{$driver}", ['driver' => $driver]);
|
||||
$mockStorage = \Mockery::mock(Filesystem::class);
|
||||
$mockStorage->shouldReceive('put')->andReturn(true);
|
||||
$gateway = createGateway([$driver => $driver]);
|
||||
Storage::shouldReceive('disk')->with($driver)->andReturn($mockStorage);
|
||||
|
||||
$gateway = createGateway();
|
||||
$disk = new DiskName($driver, $driver);
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'test.txt',
|
||||
|
|
@ -163,6 +188,83 @@ describe('StorageGatewayImpl storeFile', function (): void {
|
|||
'ftp driver' => 'ftp',
|
||||
'sftp driver' => 'sftp',
|
||||
]);
|
||||
|
||||
it('stores a file onto a scoped disk resolving underlying s3 driver', function (): void {
|
||||
// GIVEN
|
||||
config()->set('filesystems.disks.s3', ['driver' => 's3']);
|
||||
config()->set('filesystems.disks.scoped_s3', [
|
||||
'driver' => 'scoped',
|
||||
'disk' => 's3',
|
||||
'prefix' => 'media',
|
||||
]);
|
||||
$mockStorage = \Mockery::mock(Filesystem::class);
|
||||
$mockStorage->shouldReceive('put')->andReturn(true);
|
||||
$gateway = createGateway(['scoped_s3' => 'scoped']);
|
||||
Storage::shouldReceive('disk')->with('scoped_s3')->andReturn($mockStorage);
|
||||
|
||||
$disk = new DiskName('scoped_s3', 'scoped');
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'photo.jpg',
|
||||
sanitizedName: 'photo.jpg',
|
||||
contents: FileStream::fromString('image bytes')
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$path = $gateway->storeFile($disk, $sanitizedFile);
|
||||
|
||||
// THEN
|
||||
expect($path)->toBeInstanceOf(StorageFilePath::class)
|
||||
->and($path->value)->toMatch('#^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jpg$#');
|
||||
});
|
||||
|
||||
it('stores a file onto a scoped disk resolving underlying local driver', function (): void {
|
||||
// GIVEN
|
||||
config()->set('filesystems.disks.local_parent', ['driver' => 'local']);
|
||||
config()->set('filesystems.disks.scoped_local', [
|
||||
'driver' => 'scoped',
|
||||
'disk' => 'local_parent',
|
||||
'prefix' => 'uploads',
|
||||
]);
|
||||
$mockStorage = \Mockery::mock(Filesystem::class);
|
||||
$mockStorage->shouldReceive('put')->andReturn(true);
|
||||
$gateway = createGateway(['scoped_local' => 'scoped']);
|
||||
Storage::shouldReceive('disk')->with('scoped_local')->andReturn($mockStorage);
|
||||
|
||||
$disk = new DiskName('scoped_local', 'scoped');
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'doc.pdf',
|
||||
sanitizedName: 'doc.pdf',
|
||||
contents: FileStream::fromString('pdf data')
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$path = $gateway->storeFile($disk, $sanitizedFile);
|
||||
|
||||
// THEN
|
||||
expect($path)->toBeInstanceOf(StorageFilePath::class)
|
||||
->and($path->value)->toStartWith(gmdate('Y') . '/');
|
||||
});
|
||||
|
||||
it('rejects a disk that is neither default nor eligible', function (): void {
|
||||
// GIVEN
|
||||
$gateway = createGateway();
|
||||
Storage::fake('unknown');
|
||||
$disk = new DiskName('unknown', 'local');
|
||||
$sanitizedFile = new SanitizedFile(
|
||||
originalName: 'doc.pdf',
|
||||
sanitizedName: 'doc.pdf',
|
||||
contents: FileStream::fromString('data')
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$action = fn () => $gateway->storeFile($disk, $sanitizedFile);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(
|
||||
UnexpectedStorageException::class,
|
||||
'Disk unknown is neither the default nor an eligible disk'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('StorageGatewayImpl getFile', function (): void {
|
||||
|
|
@ -320,6 +422,7 @@ describe('StorageGatewayImpl logging', function (): void {
|
|||
$logger->shouldReceive('info')->with(\Mockery::pattern('/^File deletion result for disk:.*: success$/'))->once();
|
||||
|
||||
$gateway = new StorageGatewayImpl(
|
||||
config: createGatewayConfig(),
|
||||
logger: $logger,
|
||||
datePathGenerator: new DateHashPathGenerator(),
|
||||
uuidPathGenerator: new UuidPathGenerator()
|
||||
|
|
@ -346,6 +449,7 @@ describe('StorageGatewayImpl logging', function (): void {
|
|||
$logger->shouldReceive('debug')->with('File not found on disk: local, path: not_found.txt')->once();
|
||||
|
||||
$gateway = new StorageGatewayImpl(
|
||||
config: createGatewayConfig(),
|
||||
logger: $logger,
|
||||
datePathGenerator: new DateHashPathGenerator(),
|
||||
uuidPathGenerator: new UuidPathGenerator()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Uwil\Storage\Tests\Unit\Lib;
|
||||
|
||||
use Uwil\Storage\Config\StorageServiceConfig;
|
||||
use Uwil\Storage\Exceptions\UnexpectedStorageException;
|
||||
use Uwil\Storage\Lib\Utils;
|
||||
|
||||
covers(Utils::class);
|
||||
|
|
@ -99,3 +100,108 @@ describe('Utils isExtensionAllowed', function (): void {
|
|||
expect($fallback)->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Utils resolveDriver', function (): void {
|
||||
it('returns direct driver for non-scoped disk', function (): void {
|
||||
// GIVEN
|
||||
config()->set('filesystems.disks.my_s3', ['driver' => 's3']);
|
||||
|
||||
// WHEN
|
||||
$driver = Utils::resolveDriver('my_s3');
|
||||
|
||||
// THEN
|
||||
expect($driver)->toBe('s3');
|
||||
});
|
||||
|
||||
it('resolves parent driver for scoped disk from string', function (): void {
|
||||
// GIVEN
|
||||
config()->set('filesystems.disks.parent_local', ['driver' => 'local']);
|
||||
config()->set('filesystems.disks.scoped_disk', [
|
||||
'driver' => 'scoped',
|
||||
'disk' => 'parent_local',
|
||||
]);
|
||||
|
||||
// WHEN
|
||||
$driver = Utils::resolveDriver('scoped_disk');
|
||||
|
||||
// THEN
|
||||
expect($driver)->toBe('local');
|
||||
});
|
||||
|
||||
it('resolves parent driver for scoped disk when driver is given explicitly', function (): void {
|
||||
// GIVEN
|
||||
config()->set('filesystems.disks.parent_s3', ['driver' => 's3']);
|
||||
config()->set('filesystems.disks.scoped_s3', [
|
||||
'driver' => 'scoped',
|
||||
'disk' => 'parent_s3',
|
||||
]);
|
||||
|
||||
// WHEN
|
||||
$driver = Utils::resolveDriver('scoped_s3', 'scoped');
|
||||
|
||||
// THEN
|
||||
expect($driver)->toBe('s3');
|
||||
});
|
||||
|
||||
it('returns explicit non-scoped driver without reading config', function (): void {
|
||||
// WHEN
|
||||
$driver = Utils::resolveDriver('not_configured', 'sftp');
|
||||
|
||||
// THEN
|
||||
expect($driver)->toBe('sftp');
|
||||
});
|
||||
|
||||
it('throws exception when parent disk has no driver configured', function (): void {
|
||||
// GIVEN
|
||||
config()->set('filesystems.disks.scoped_orphan', [
|
||||
'driver' => 'scoped',
|
||||
'disk' => 'missing_parent',
|
||||
]);
|
||||
|
||||
// WHEN
|
||||
$action = fn () => Utils::resolveDriver('scoped_orphan');
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(
|
||||
UnexpectedStorageException::class,
|
||||
"Parent disk 'missing_parent' of scoped disk 'scoped_orphan' has no valid driver configured."
|
||||
);
|
||||
});
|
||||
|
||||
it('throws exception when scoped disk has missing parent disk', function (): void {
|
||||
// GIVEN
|
||||
config()->set('filesystems.disks.scoped_invalid', [
|
||||
'driver' => 'scoped',
|
||||
]);
|
||||
|
||||
// WHEN
|
||||
$action = fn () => Utils::resolveDriver('scoped_invalid');
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(
|
||||
UnexpectedStorageException::class,
|
||||
"Scoped disk 'scoped_invalid' is missing a valid parent 'disk' configuration."
|
||||
);
|
||||
});
|
||||
|
||||
it('throws exception when scoped disk has circular reference', function (): void {
|
||||
// GIVEN
|
||||
config()->set('filesystems.disks.scoped_x', [
|
||||
'driver' => 'scoped',
|
||||
'disk' => 'scoped_y',
|
||||
]);
|
||||
config()->set('filesystems.disks.scoped_y', [
|
||||
'driver' => 'scoped',
|
||||
'disk' => 'scoped_x',
|
||||
]);
|
||||
|
||||
// WHEN
|
||||
$action = fn () => Utils::resolveDriver('scoped_x');
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(
|
||||
UnexpectedStorageException::class,
|
||||
"Circular reference detected in scoped disk 'scoped_y'."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@ describe('DiskName instantiation', function (): void {
|
|||
->and($vo->value)->toBe('local')
|
||||
->and($vo->driver)->toBe($driver);
|
||||
})->with([
|
||||
'local driver' => 'local',
|
||||
'ftp driver' => 'ftp',
|
||||
'sftp driver' => 'sftp',
|
||||
's3 driver' => 's3',
|
||||
'local driver' => 'local',
|
||||
'ftp driver' => 'ftp',
|
||||
'sftp driver' => 'sftp',
|
||||
's3 driver' => 's3',
|
||||
'scoped driver' => 'scoped',
|
||||
]);
|
||||
|
||||
it('can instantiate with a BackedEnum value', function (): void {
|
||||
|
|
|
|||
|
|
@ -145,6 +145,22 @@ describe('StorageFilePath validation', function (): void {
|
|||
]);
|
||||
});
|
||||
|
||||
describe('StorageFilePath json serialization', function (): void {
|
||||
it('returns the path when json serialized', function (): void {
|
||||
// GIVEN
|
||||
$path = 'documents/archive/2026.zip';
|
||||
$vo = new StorageFilePath($path);
|
||||
|
||||
// WHEN
|
||||
$jsonSerialized = $vo->jsonSerialize();
|
||||
$jsonEncoded = json_encode($vo);
|
||||
|
||||
// THEN
|
||||
expect($jsonSerialized)->toBe($path)
|
||||
->and($jsonEncoded)->toBe('"documents\/archive\/2026.zip"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('StorageFilePath string representation', function (): void {
|
||||
it('returns the path when cast to string or via __toString', function (): void {
|
||||
// GIVEN
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue