├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── front-lint.yml │ └── php-lint.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── UPGRADING.md ├── composer.json ├── config └── nova-file-manager.php ├── dist ├── .vite │ └── manifest.json ├── css │ ├── field.css │ └── tool.css └── js │ ├── package.js │ └── tool.js ├── duster.json ├── lang ├── en.json └── ua.json ├── package-lock.json ├── package.json ├── pint.json ├── postcss.config.cjs ├── resources ├── css │ └── tool.css └── js │ ├── @types │ └── index.ts │ ├── components │ ├── Browser.vue │ ├── BrowserBreadcrumbs.vue │ ├── BrowserContent.vue │ ├── BrowserDropzone.vue │ ├── BrowserFile.vue │ ├── BrowserFolder.vue │ ├── BrowserModal.vue │ ├── BrowserPagination.vue │ ├── BrowserToolbar.vue │ ├── Elements │ │ ├── Button.vue │ │ ├── DetailView.vue │ │ ├── Dropdown.vue │ │ ├── DropdownMenu.vue │ │ ├── Empty.vue │ │ └── SelectControl.vue │ ├── Modals │ │ ├── BaseModal.vue │ │ ├── BrowserDetailModal.vue │ │ ├── BrowserSelectedModal.vue │ │ ├── CreateFolderModal.vue │ │ ├── CropModal.vue │ │ ├── DeleteModal.vue │ │ ├── QueueModal.vue │ │ └── RenameModal.vue │ └── ToolbarButton.vue │ ├── constants │ └── index.ts │ ├── field │ ├── DetailField.vue │ ├── FormField.vue │ └── IndexField.vue │ ├── helpers │ ├── csrf.ts │ ├── data-transfer.ts │ ├── mime-icons.ts │ ├── transformers.ts │ └── truncate.ts │ ├── package.js │ ├── pages │ └── Tool.vue │ ├── stores │ └── browser.ts │ └── tool.js ├── routes ├── api.php └── inertia.php ├── screenshots ├── tool-detail-dark.png ├── tool-detail.png ├── tool-inside-dark.png ├── tool-inside.png ├── tool-list.png └── tool.png ├── src ├── Casts │ ├── Asset.php │ └── AssetCollection.php ├── Contracts │ ├── Entities │ │ └── Entity.php │ ├── Filesystem │ │ └── Upload │ │ │ └── Uploader.php │ ├── Services │ │ └── FileManagerContract.php │ └── Support │ │ ├── InteractsWithFilesystem.php │ │ └── ResolvesUrl.php ├── Entities │ ├── Entity.php │ ├── File.php │ ├── Image.php │ ├── Text.php │ └── Video.php ├── Events │ ├── FileDeleted.php │ ├── FileDeleting.php │ ├── FileDuplicated.php │ ├── FileDuplicating.php │ ├── FileRenamed.php │ ├── FileRenaming.php │ ├── FileUnzipped.php │ ├── FileUnzipping.php │ ├── FileUploaded.php │ ├── FileUploading.php │ ├── FolderCreated.php │ ├── FolderCreating.php │ ├── FolderDeleted.php │ ├── FolderDeleting.php │ ├── FolderRenamed.php │ └── FolderRenaming.php ├── FileManager.php ├── FileManagerTool.php ├── Filesystem │ └── Upload │ │ └── Uploader.php ├── Http │ ├── Controllers │ │ ├── DiskController.php │ │ ├── FileController.php │ │ ├── FolderController.php │ │ ├── IndexController.php │ │ └── ToolController.php │ ├── Middleware │ │ └── Authorize.php │ └── Requests │ │ ├── BaseRequest.php │ │ ├── CreateFolderRequest.php │ │ ├── DeleteFileRequest.php │ │ ├── DeleteFolderRequest.php │ │ ├── DuplicateFileRequest.php │ │ ├── IndexRequest.php │ │ ├── RenameFileRequest.php │ │ ├── RenameFolderRequest.php │ │ ├── UnzipFileRequest.php │ │ └── UploadFileRequest.php ├── Rules │ ├── DiskExistsRule.php │ ├── ExistsInFilesystem.php │ ├── FileLimit.php │ ├── FileMissingInFilesystem.php │ └── MissingInFilesystem.php ├── Services │ ├── FileManagerService.php │ └── MimeTypes.php ├── Support │ └── Asset.php ├── ToolServiceProvider.php ├── Traits │ └── Support │ │ ├── InteractsWithFilesystem.php │ │ └── ResolvesUrl.php └── helpers.php ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "composer" 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | # Always increase the version requirement 13 | # to match the new version. 14 | versioning-strategy: increase 15 | -------------------------------------------------------------------------------- /.github/workflows/front-lint.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/duster.yml 2 | name: Front lint 3 | 4 | on: 5 | push: 6 | branches: main 7 | pull_request: 8 | 9 | env: 10 | NODE_VERSION: 20.10.0 11 | 12 | jobs: 13 | application-test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Setup Node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{env.NODE_VERSION}} 23 | cache: "npm" 24 | 25 | - name: "Setup PHP" 26 | uses: "shivammathur/setup-php@v2" 27 | with: 28 | php-version: "latest" 29 | 30 | - name: "composer install" 31 | run: composer install --no-cache --ignore-platform-reqs --no-interaction --prefer-dist --optimize-autoloader --apcu-autoloader 32 | 33 | 34 | - run: npm i 35 | - run: npm run nova:install 36 | - run: npm run build 37 | -------------------------------------------------------------------------------- /.github/workflows/php-lint.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/duster.yml 2 | name: PHP Lint 3 | 4 | on: 5 | push: 6 | branches: main 7 | pull_request: 8 | 9 | jobs: 10 | php-lint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: "Setup PHP" 17 | uses: "shivammathur/setup-php@v2" 18 | with: 19 | php-version: "latest" 20 | 21 | - name: "composer install" 22 | run: composer install --no-cache --ignore-platform-reqs --no-interaction --prefer-dist --optimize-autoloader --apcu-autoloader 23 | 24 | - name: "duster" 25 | uses: tighten/duster-action@v2 26 | with: 27 | args: lint --using=phpstan,tlint,phpcs,pint 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Mac OS X 4 | .DS_Store 5 | ._.* 6 | ._* 7 | 8 | # Ignore local editor 9 | .project 10 | .settings 11 | /.idea 12 | /.vscode 13 | *.swp 14 | tags 15 | nbproject/* 16 | 17 | # Windows 18 | Thumbs.db 19 | 20 | auth.json 21 | npm-debug.log 22 | yarn-error.log 23 | 24 | .env 25 | .env.backup 26 | phpunit.xml 27 | .phpunit.result.cache 28 | docker-compose.override.yml 29 | Homestead.json 30 | Homestead.yaml 31 | 32 | config/version.yml 33 | 34 | composer.phar 35 | composer.lock 36 | yarn.lock 37 | 38 | _ide_helper.php 39 | _ide_helper_models.php 40 | .phpstorm.meta.php 41 | lighthouse-audit* 42 | .nova 43 | 44 | /node_modules 45 | /vendor 46 | /packages 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## v3.0 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Artem Stepanenko 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # File manager tool for Laravel Nova 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/stepanenko3/nova-filemanager.svg?style=flat-square)](https://packagist.org/packages/stepanenko3/nova-filemanager) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/stepanenko3/nova-filemanager.svg?style=flat-square)](https://packagist.org/packages/stepanenko3/nova-filemanager) 5 | [![License](https://poser.pugx.org/stepanenko3/nova-filemanager/license)](https://packagist.org/packages/stepanenko3/nova-filemanager) 6 | 7 | A File manager Tool and Field for Laravel Nova 8 | 9 | ![screenshot of tool](screenshots/tool.png) 10 | 11 | ## Installation 12 | 13 | ```bash 14 | composer require stepanenko3/nova-filemanager 15 | ``` 16 | 17 | ## Screenshots 18 | 19 | ![screenshot of tool](screenshots/tool-inside.png) 20 | ![screenshot of tool](screenshots/tool-inside-dark.png) 21 | ![screenshot of tool](screenshots/tool-list.png) 22 | ![screenshot of tool](screenshots/tool-detail.png) 23 | ![screenshot of tool](screenshots/tool-detail-dark.png) 24 | 25 | ### Changelog 26 | 27 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 28 | 29 | ### Security 30 | 31 | If you discover any security related issues, please using the issue tracker. 32 | 33 | ## Credits 34 | 35 | - [Artem Stepanenko](https://github.com/stepanenko3) 36 | - [Eric Lagarda](https://github.com/Krato) 37 | - [Spatie Nova Tool Skeleton](https://github.com/spatie/skeleton-nova-tool) 38 | 39 | ## Contributing 40 | 41 | Thank you for considering contributing to this package! Please create a pull request with your contributions with detailed explanation of the changes you are proposing. 42 | 43 | ## License 44 | 45 | This package is open-sourced software licensed under the [MIT license](LICENSE.md). 46 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | 3 | ## To 1.0.0 4 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stepanenko3/nova-filemanager", 3 | "description": "File manager tool for Laravel Nova", 4 | "keywords": [ 5 | "laravel", 6 | "nova", 7 | "filemanager" 8 | ], 9 | "license": "MIT", 10 | "require": { 11 | "php": ">=7.1.0", 12 | "laravel/nova": "^4.0", 13 | "pion/laravel-chunk-upload": "^1.5", 14 | "stepanenko3/laravel-helpers": "*", 15 | "stepanenko3/laravel-pagination": "*" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "Stepanenko3\\NovaFileManager\\": "src/" 20 | }, 21 | "files": [ 22 | "src/helpers.php" 23 | ] 24 | }, 25 | "extra": { 26 | "laravel": { 27 | "providers": [ 28 | "Stepanenko3\\NovaFileManager\\ToolServiceProvider" 29 | ] 30 | } 31 | }, 32 | "config": { 33 | "sort-packages": true 34 | }, 35 | "minimum-stability": "dev", 36 | "prefer-stable": true, 37 | "repositories": [ 38 | { 39 | "type": "composer", 40 | "url": "https://laravelsatis.com" 41 | } 42 | ], 43 | "require-dev": { 44 | "phpstan/phpstan": "^1.10", 45 | "tightenco/duster": "^2.7" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/nova-file-manager.php: -------------------------------------------------------------------------------- 1 | 'file-manager', 5 | 6 | 'mime_types' => [ 7 | 'image' => [ 8 | 'image/', 9 | 'svg', 10 | ], 11 | 'audio' => [ 12 | 'audio/', 13 | ], 14 | 'video' => [ 15 | 'video/', 16 | ], 17 | 'pdf' => [ 18 | 'application/pdf', 19 | ], 20 | 'archive' => [ 21 | 'zip', 22 | 'rar', 23 | 'tar', 24 | 'gz', 25 | '7z', 26 | 'pkg', 27 | 'application/x-compressed', 28 | ], 29 | 'text' => [ 30 | 'text/', 31 | 'rtf', 32 | 'json', 33 | 'javascript', 34 | '/xml', 35 | 'sql', 36 | ], 37 | 'word' => [ 38 | 'wordprocessingml', 39 | ], 40 | ], 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | File manager filters 45 | |-------------------------------------------------------------------------- 46 | | This option let you to filter your files by extensions. 47 | | You can create|modify|delete as you want. 48 | */ 49 | 50 | 'filters' => [ 51 | 'images' => ['jpg', 'jpeg', 'png', 'gif', 'svg', 'bmp', 'tiff'], 52 | 53 | 'documents' => ['json', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pps', 'pptx', 'odt', 'rtf', 'md', 'txt', 'css'], 54 | 55 | 'videos' => ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv', '3gp', 'h264'], 56 | 57 | 'audios' => ['mp3', 'ogg', 'wav', 'wma', 'midi'], 58 | 59 | 'archive' => ['zip', 'rar', 'tar', 'gz', '7z', 'pkg'], 60 | ], 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | File manager default filter 65 | |-------------------------------------------------------------------------- 66 | | This will set the default filter for all your File manager. You should use one 67 | | of the keys used in filters in lowercase. If you have a key called Documents, 68 | | use 'documents' as your default filter. Default to false 69 | */ 70 | 'filter' => false, 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Default Disk 75 | |-------------------------------------------------------------------------- 76 | | 77 | | Can be used to set the default disk used by the tool. 78 | | When no disk is selected, the tool will use the default public disk. 79 | | 80 | | default: public 81 | */ 82 | 'default_disk' => env('NOVA_FILE_MANAGER_DISK', 'public'), 83 | 84 | /* 85 | |-------------------------------------------------------------------------- 86 | | Available disks 87 | |-------------------------------------------------------------------------- 88 | | 89 | | Can be used to specify the filesystem disks that can be available in the 90 | | tool. Note that the default disk (in this case "PUBLIC") is required to 91 | | be in this array. 92 | | 93 | | The disks should be defined in the filesystems.php config. 94 | | 95 | */ 96 | 'available_disks' => [ 97 | 'public', 98 | 'upload', 99 | // 's3', 100 | // 'ftp', 101 | // ... more disks 102 | ], 103 | 104 | /* 105 | |-------------------------------------------------------------------------- 106 | | Show hidden files 107 | |-------------------------------------------------------------------------- 108 | | 109 | | Toggle whether the tool should show the files and directories which name 110 | | starts with "." 111 | | 112 | | default: false 113 | */ 114 | 'show_hidden_files' => env('NOVA_FILE_MANAGER_SHOW_HIDDEN_FILES', false), 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | Human readable file size 119 | |-------------------------------------------------------------------------- 120 | | 121 | | If set to true, the tool will display the file size in a parsed and more 122 | | readable format. Otherwise, it will display the raw byte size. 123 | | 124 | | default: true 125 | */ 126 | 'human_readable_size' => env('NOVA_FILE_MANAGER_HUMAN_READABLE_SIZE', true), 127 | 128 | /* 129 | |-------------------------------------------------------------------------- 130 | | Human readable timestamps 131 | |-------------------------------------------------------------------------- 132 | | 133 | | If set to true, the tool will display datetime string in a human-readable 134 | | difference format. Otherwise, it will display the regular datetime value. 135 | | 136 | | default: true 137 | */ 138 | 'human_readable_datetime' => env('NOVA_FILE_MANAGER_HUMAN_READABLE_DATETIME', true), 139 | 140 | /* 141 | |-------------------------------------------------------------------------- 142 | | Entities map 143 | |-------------------------------------------------------------------------- 144 | | 145 | | Here you can override or define new entity types that can be used to map 146 | | the files in your storage. 147 | | 148 | | Should extend \Stepanenko3\NovaFileManager\Entities\Entity::class 149 | | 150 | */ 151 | 'entities' => [ 152 | 'image' => \Stepanenko3\NovaFileManager\Entities\Image::class, 153 | 'video' => \Stepanenko3\NovaFileManager\Entities\Video::class, 154 | 'text' => \Stepanenko3\NovaFileManager\Entities\Text::class, 155 | 'default' => \Stepanenko3\NovaFileManager\Entities\File::class, 156 | ], 157 | 158 | /* 159 | |-------------------------------------------------------------------------- 160 | | URL Signing 161 | |-------------------------------------------------------------------------- 162 | | 163 | | When using a cloud filesystem disk (e.g. S3), you may wish to provide 164 | | signed url through the tool. You can enable the setting, and adjust the 165 | | signing configuration. 166 | | 167 | | Uses: Storage::temporaryUrl() 168 | */ 169 | 'url_signing' => [ 170 | 'enabled' => env('NOVA_FILE_MANAGER_URL_SIGNING_ENABLED', false), 171 | 'unit' => 'minutes', 172 | 'value' => 10, 173 | ], 174 | 175 | /* 176 | |-------------------------------------------------------------------------- 177 | | Update checker 178 | |-------------------------------------------------------------------------- 179 | | 180 | | The tool provides a handy update checker that will notify you when a new 181 | | version is available. You can disable it if you don't want to receive 182 | | these notifications. 183 | | 184 | */ 185 | 'update_checker' => [ 186 | 'enabled' => env('NOVA_FILE_MANAGER_UPDATE_CHECKER_ENABLED', true), 187 | 'ttl_in_days' => env('NOVA_FILE_MANAGER_UPDATE_CHECKER_TTL_IN_DAYS', 1), 188 | ], 189 | ]; 190 | -------------------------------------------------------------------------------- /dist/.vite/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources/js/package.js": { 3 | "file": "js/package.js", 4 | "name": "package", 5 | "src": "resources/js/package.js", 6 | "isEntry": true 7 | }, 8 | "style.css": { 9 | "file": "css/tool.css", 10 | "src": "style.css" 11 | } 12 | } -------------------------------------------------------------------------------- /duster.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "bootstrap", 4 | "config" 5 | ], 6 | "scripts": { 7 | "lint": { 8 | "phpstan": [ 9 | "./vendor/bin/phpstan", 10 | "analyse", 11 | "src", 12 | "--memory-limit=1G" 13 | ] 14 | } 15 | }, 16 | "processTimeout": 120 17 | } 18 | -------------------------------------------------------------------------------- /lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Folders": "Folders", 3 | "Files": "Files", 4 | "Details": "Details", 5 | "Crop": "Crop", 6 | "Unarchive": "Unarchive", 7 | "Duplicate": "Duplicate", 8 | "Rename": "Rename", 9 | "Delete": "Delete", 10 | "Refresh": "Refresh", 11 | "Create folder": "Create folder", 12 | "Upload a file": "Upload a file", 13 | "Cancel": "Cancel", 14 | "Create": "Create", 15 | "Write a folder name": "Write a folder name", 16 | "Rename folder": "Rename folder", 17 | "Select": "Select", 18 | "Mime": "Mime", 19 | "Size": "Size", 20 | "Dimensions": "Dimensions", 21 | "Aspect Ratio": "Aspect Ratio", 22 | "Url": "Url", 23 | "Download": "Download", 24 | "Search": "Search", 25 | "Per page": "Per page", 26 | "Period": "Period", 27 | "Sort by": "Sort by", 28 | "Date Asc": "Date asc", 29 | "Date Desc": "Date desc", 30 | "Name Asc": "Name asc", 31 | "Name Desc": "Name desc", 32 | "Size Asc": "Size asc", 33 | "Size Desc": "Size desc", 34 | "Rename file": "Rename file", 35 | "Crop image": "Crop image", 36 | "Confirm crop": "Confirm crop", 37 | "No files were found for your request": "No files were found for your request", 38 | "You can upload a new file or create a new folder": "You can upload a new file or create a new folder", 39 | "Are you sure you want to remove this file?": "Are you sure you want to remove this file?", 40 | "Remember: The file will be delete from your storage": "Remember: The file will be delete from your storage", 41 | "Are you sure you want to remove this files?": "Are you sure you want to remove this files?", 42 | "Remember: The files will be delete from your storage": "Remember: The files will be delete from your storage" 43 | } 44 | -------------------------------------------------------------------------------- /lang/ua.json: -------------------------------------------------------------------------------- 1 | { 2 | "Folders": "Папки", 3 | "Files": "Файли", 4 | "Details": "Деталі", 5 | "Crop": "Обрізати", 6 | "Unarchive": "Розархивувати", 7 | "Duplicate": "Продублювати", 8 | "Rename": "Перейменувати", 9 | "Delete": "Видалити", 10 | "Refresh": "Оновити", 11 | "Create folder": "Створити папку", 12 | "Upload a file": "Завантажити файл", 13 | "Cancel": "Відмінити", 14 | "Create": "Створити", 15 | "Write a folder name": "Напишіть назву папки", 16 | "Rename folder": "Перейменувати папку", 17 | "Select": "Обрати", 18 | "Mime": "Тип", 19 | "Size": "Розмір", 20 | "Dimensions": "Розміри", 21 | "Aspect Ratio": "Співвідношення сторін", 22 | "Url": "Посилання", 23 | "Download": "Завантажити", 24 | "Search": "Пошук", 25 | "Per page": "На сторінку", 26 | "Period": "Період", 27 | "Sort by": "Сортувати", 28 | "Date Asc": "Спочатку нові", 29 | "Date Desc": "Спочатку старі", 30 | "Name Asc": "Назва A-Я", 31 | "Name Desc": "Назва Я-А", 32 | "Size Asc": "Розмір 0-9", 33 | "Size Desc": "Розмір 9-0", 34 | "Rename file": "Перейменувати файл", 35 | "Crop image": "Обрізати зображення", 36 | "Confirm crop": "Підтвердити", 37 | "No files were found for your request": "По вашому запиту не знайдено файлів", 38 | "You can upload a new file or create a new folder": "Ви можете завантажити файл, або створити нову папку", 39 | "Are you sure you want to remove this file?": "Ви впевнені, що хочете видалити цей файл?", 40 | "Remember: The file will be delete from your storage": "Памятайте: Файл буде видалено з вашого сховища", 41 | "Are you sure you want to remove this files?": "Ви впевнені, що хочете видалити ці файли?", 42 | "Remember: The files will be delete from your storage": "Памятайте: Файли буде видалено з вашого сховища" 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "NODE_ENV=development vite build --watch", 5 | "dev:package": "NODE_ENV=development LIB=package vite build --watch", 6 | "build": "yarn build:tool && yarn build:package", 7 | "build:tool": "LIB_NAME=tool vite build", 8 | "build:package": "LIB_NAME=package vite build", 9 | "package": "npm run format && npm run lint && npm run build", 10 | "check-format": "prettier --list-different 'resources/**/*.{css,js,vue,ts}'", 11 | "format": "prettier --write 'resources/js/**/*.{css,js,vue,ts}'", 12 | "lint": "eslint resources/js --fix --ext js,vue,ts", 13 | "nova:install": "npm --prefix='./vendor/laravel/nova' i" 14 | }, 15 | "devDependencies": { 16 | "@inertiajs/inertia": "^0.11.1", 17 | "@types/node": "^20.11.30", 18 | "@vitejs/plugin-vue": "^5.0.4", 19 | "@vue/compiler-sfc": "^3.4.21", 20 | "filesize": "^10.1.1", 21 | "md5": "^2.2.1", 22 | "postcss": "^8.4.38", 23 | "postcss-import": "^16.1.0", 24 | "sass": "^1.72.0", 25 | "tailwindcss": "^3.4.3", 26 | "uuid": "^9.0.1", 27 | "vite": "^5.2.6", 28 | "vue-cropperjs": "^5.0.0" 29 | }, 30 | "dependencies": { 31 | "@pqina/pintura": "^8.77.0", 32 | "@pqina/vue-pintura": "^9.0.1", 33 | "@types/lodash": "^4.14.195", 34 | "@types/uuid": "^9.0.2", 35 | "@vueuse/core": "^10.1.2", 36 | "autoprefixer": "^10.4.14", 37 | "lodash": "^4.17.21", 38 | "pinia": "^2.1.7", 39 | "resumablejs": "^1.1.0", 40 | "tailwind-scrollbar-hide": "^1.1.7", 41 | "vue": "^3.4.21" 42 | }, 43 | "files": [ 44 | "dist/*" 45 | ], 46 | "type": "module", 47 | "main": "./dist/js/tool.js", 48 | "module": "./dist/js/package.js", 49 | "exports": { 50 | ".": { 51 | "import": "./dist/js/package.js", 52 | "require": "./dist/js/package.js" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "@PSR12": true, 5 | "@PSR12:risky": true, 6 | "@PHP80Migration": true, 7 | "@PHP80Migration:risky": true, 8 | "@PHP81Migration": true, 9 | "align_multiline_comment": { 10 | "comment_type": "phpdocs_only" 11 | }, 12 | "array_indentation": true, 13 | "array_syntax": { 14 | "syntax": "short" 15 | }, 16 | "blank_line_after_namespace": true, 17 | "blank_line_after_opening_tag": true, 18 | "blank_line_before_statement": { 19 | "statements": [ 20 | "break", 21 | "case", 22 | "continue", 23 | "declare", 24 | "default", 25 | "exit", 26 | "goto", 27 | "include", 28 | "include_once", 29 | "phpdoc", 30 | "require", 31 | "require_once", 32 | "return", 33 | "switch", 34 | "throw", 35 | "try", 36 | "yield", 37 | "yield_from" 38 | ] 39 | }, 40 | "blank_line_between_import_groups": true, 41 | "braces": { 42 | "allow_single_line_anonymous_class_with_empty_body": true, 43 | "allow_single_line_closure": true 44 | }, 45 | "cast_spaces": { 46 | "space": "single" 47 | }, 48 | "class_attributes_separation": { 49 | "elements": { 50 | "method": "one" 51 | } 52 | }, 53 | "class_definition": { 54 | "single_line": true, 55 | "space_before_parenthesis": true 56 | }, 57 | "class_reference_name_casing": true, 58 | "clean_namespace": true, 59 | "combine_consecutive_issets": true, 60 | "combine_consecutive_unsets": true, 61 | "compact_nullable_typehint": true, 62 | "concat_space": { 63 | "spacing": "one" 64 | }, 65 | "constant_case": { 66 | "case": "lower" 67 | }, 68 | "declare_equal_normalize": { 69 | "space": "single" 70 | }, 71 | "echo_tag_syntax": true, 72 | "elseif": true, 73 | "empty_loop_body": true, 74 | "empty_loop_condition": true, 75 | "encoding": true, 76 | "escape_implicit_backslashes": true, 77 | "explicit_indirect_variable": true, 78 | "explicit_string_variable": true, 79 | "full_opening_tag": true, 80 | "fully_qualified_strict_types": true, 81 | "function_declaration": true, 82 | "function_typehint_space": true, 83 | "general_phpdoc_tag_rename": { 84 | "replacements": { 85 | "inheritDocs": "inheritDoc" 86 | } 87 | }, 88 | "heredoc_to_nowdoc": true, 89 | "include": true, 90 | "increment_style": { 91 | "style": "post" 92 | }, 93 | "indentation_type": true, 94 | "integer_literal_case": true, 95 | "lambda_not_used_import": true, 96 | "line_ending": true, 97 | "linebreak_after_opening_tag": true, 98 | "lowercase_cast": true, 99 | "lowercase_keywords": true, 100 | "lowercase_static_reference": true, 101 | "magic_constant_casing": true, 102 | "magic_method_casing": true, 103 | "method_argument_space": { 104 | "on_multiline": "ensure_fully_multiline", 105 | "after_heredoc": true, 106 | "keep_multiple_spaces_after_comma": true, 107 | "attribute_placement": "standalone" 108 | }, 109 | "method_chaining_indentation": true, 110 | "multiline_comment_opening_closing": true, 111 | "multiline_whitespace_before_semicolons": { 112 | "strategy": "no_multi_line" 113 | }, 114 | "native_function_casing": true, 115 | "native_function_type_declaration_casing": true, 116 | "new_with_braces": { 117 | "anonymous_class": true, 118 | "named_class": true 119 | }, 120 | "no_alias_language_construct_call": true, 121 | "no_alternative_syntax": true, 122 | "no_binary_string": true, 123 | "no_blank_lines_after_class_opening": true, 124 | "no_blank_lines_after_phpdoc": true, 125 | "no_break_comment": true, 126 | "no_closing_tag": true, 127 | "no_empty_phpdoc": true, 128 | "no_empty_statement": true, 129 | "no_extra_blank_lines": { 130 | "tokens": [ 131 | "attribute", 132 | "break", 133 | "case", 134 | "continue", 135 | "curly_brace_block", 136 | "default", 137 | "extra", 138 | "parenthesis_brace_block", 139 | "return", 140 | "square_brace_block", 141 | "switch", 142 | "throw", 143 | "use" 144 | ] 145 | }, 146 | "no_leading_import_slash": true, 147 | "no_leading_namespace_whitespace": true, 148 | "no_mixed_echo_print": true, 149 | "no_multiline_whitespace_around_double_arrow": true, 150 | "no_null_property_initialization": true, 151 | "no_short_bool_cast": true, 152 | "no_singleline_whitespace_before_semicolons": true, 153 | "no_space_around_double_colon": true, 154 | "no_spaces_after_function_name": true, 155 | "no_spaces_around_offset": true, 156 | "no_spaces_inside_parenthesis": true, 157 | "no_superfluous_elseif": true, 158 | "no_superfluous_phpdoc_tags": { 159 | "allow_mixed": true, 160 | "allow_unused_params": true 161 | }, 162 | "no_trailing_comma_in_singleline": true, 163 | "no_trailing_whitespace": true, 164 | "no_trailing_whitespace_in_comment": true, 165 | "no_unneeded_control_parentheses": { 166 | "statements": [ 167 | "break", 168 | "clone", 169 | "continue", 170 | "echo_print", 171 | "negative_instanceof", 172 | "others", 173 | "return", 174 | "switch_case", 175 | "yield", 176 | "yield_from" 177 | ] 178 | }, 179 | "no_unneeded_curly_braces": { 180 | "namespaces": true 181 | }, 182 | "no_unneeded_import_alias": true, 183 | "no_unset_cast": true, 184 | "no_unused_imports": true, 185 | "no_useless_else": true, 186 | "no_useless_nullsafe_operator": true, 187 | "no_useless_return": true, 188 | "no_whitespace_before_comma_in_array": true, 189 | "no_whitespace_in_blank_line": true, 190 | "normalize_index_brace": true, 191 | "object_operator_without_whitespace": true, 192 | "operator_linebreak": { 193 | "position": "beginning", 194 | "only_booleans": true 195 | }, 196 | "ordered_class_elements": true, 197 | "ordered_interfaces": true, 198 | "ordered_imports": true, 199 | "phpdoc_add_missing_param_annotation": true, 200 | "phpdoc_align": { 201 | "align": "left" 202 | }, 203 | "phpdoc_annotation_without_dot": true, 204 | "phpdoc_indent": true, 205 | "phpdoc_inline_tag_normalizer": true, 206 | "phpdoc_no_access": true, 207 | "phpdoc_no_alias_tag": true, 208 | "phpdoc_no_empty_return": true, 209 | "phpdoc_no_package": true, 210 | "phpdoc_no_useless_inheritdoc": true, 211 | "phpdoc_order": { 212 | "order": [ 213 | "param", 214 | "throws", 215 | "return" 216 | ] 217 | }, 218 | "phpdoc_order_by_value": true, 219 | "phpdoc_return_self_reference": true, 220 | "phpdoc_scalar": true, 221 | "phpdoc_separation": true, 222 | "phpdoc_single_line_var_spacing": true, 223 | "phpdoc_summary": true, 224 | "phpdoc_tag_type": { 225 | "tags": { 226 | "inheritDoc": "inline" 227 | } 228 | }, 229 | "phpdoc_to_comment": true, 230 | "phpdoc_trim": true, 231 | "phpdoc_trim_consecutive_blank_line_separation": true, 232 | "phpdoc_types": true, 233 | "phpdoc_types_order": true, 234 | "phpdoc_var_annotation_correct_order": true, 235 | "phpdoc_var_without_name": true, 236 | "protected_to_private": true, 237 | "return_assignment": true, 238 | "return_type_declaration": true, 239 | "semicolon_after_instruction": true, 240 | "short_scalar_cast": true, 241 | "simple_to_complex_string_variable": true, 242 | "single_blank_line_at_eof": true, 243 | "single_blank_line_before_namespace": false, 244 | "single_class_element_per_statement": true, 245 | "single_import_per_statement": true, 246 | "single_line_after_imports": true, 247 | "single_line_comment_spacing": true, 248 | "single_line_comment_style": true, 249 | "single_line_throw": true, 250 | "single_quote": { 251 | "strings_containing_single_quote_chars": true 252 | }, 253 | "single_space_after_construct": { 254 | "constructs": [ 255 | "abstract", 256 | "as", 257 | "attribute", 258 | "break", 259 | "case", 260 | "catch", 261 | "class", 262 | "clone", 263 | "comment", 264 | "const", 265 | "const_import", 266 | "continue", 267 | "do", 268 | "echo", 269 | "else", 270 | "elseif", 271 | "enum", 272 | "extends", 273 | "final", 274 | "finally", 275 | "for", 276 | "foreach", 277 | "function", 278 | "function_import", 279 | "global", 280 | "goto", 281 | "if", 282 | "implements", 283 | "include", 284 | "include_once", 285 | "instanceof", 286 | "insteadof", 287 | "interface", 288 | "match", 289 | "named_argument", 290 | "namespace", 291 | "new", 292 | "open_tag_with_echo", 293 | "php_doc", 294 | "php_open", 295 | "print", 296 | "private", 297 | "protected", 298 | "public", 299 | "readonly", 300 | "require", 301 | "require_once", 302 | "return", 303 | "static", 304 | "switch", 305 | "throw", 306 | "trait", 307 | "try", 308 | "type_colon", 309 | "use", 310 | "use_lambda", 311 | "use_trait", 312 | "var", 313 | "while", 314 | "yield", 315 | "yield_from" 316 | ] 317 | }, 318 | "single_trait_insert_per_statement": true, 319 | "space_after_semicolon": { 320 | "remove_in_empty_for_expressions": true 321 | }, 322 | "standardize_increment": true, 323 | "standardize_not_equals": true, 324 | "switch_case_semicolon_to_colon": true, 325 | "switch_case_space": true, 326 | "switch_continue_to_break": true, 327 | "ternary_operator_spaces": true, 328 | "trailing_comma_in_multiline": true, 329 | "trim_array_spaces": true, 330 | "types_spaces": { 331 | "space": "single" 332 | }, 333 | "unary_operator_spaces": true, 334 | "visibility_required": { 335 | "elements": [ 336 | "property", 337 | "method", 338 | "const" 339 | ] 340 | }, 341 | "whitespace_after_comma_in_array": { 342 | "ensure_single_space": true 343 | }, 344 | "array_push": true, 345 | "combine_nested_dirname": true, 346 | "comment_to_phpdoc": true, 347 | "dir_constant": true, 348 | "ereg_to_preg": true, 349 | "error_suppression": true, 350 | "final_internal_class": true, 351 | "fopen_flag_order": true, 352 | "fopen_flags": { 353 | "b_mode": false 354 | }, 355 | "function_to_constant": true, 356 | "implode_call": true, 357 | "is_null": true, 358 | "logical_operators": true, 359 | "modernize_types_casting": true, 360 | "native_constant_invocation": { 361 | "fix_built_in": false, 362 | "include": [ 363 | "DIRECTORY_SEPARATOR", 364 | "PHP_INT_SIZE", 365 | "PHP_SAPI", 366 | "PHP_VERSION_ID" 367 | ], 368 | "scope": "namespaced", 369 | "strict": true 370 | }, 371 | "no_alias_functions": { 372 | "sets": [ 373 | "@all" 374 | ] 375 | }, 376 | "no_homoglyph_names": true, 377 | "no_php4_constructor": true, 378 | "no_trailing_whitespace_in_string": true, 379 | "no_unneeded_final_method": true, 380 | "no_unreachable_default_argument_value": true, 381 | "no_unset_on_property": true, 382 | "no_useless_sprintf": true, 383 | "ordered_traits": true, 384 | "pow_to_exponentiation": true, 385 | "psr_autoloading": true, 386 | "self_accessor": true, 387 | "set_type_to_cast": true, 388 | "string_length_to_empty": true, 389 | "ternary_to_elvis_operator": true, 390 | "declare_strict_types": false, 391 | "yoda_style": false, 392 | "group_import": false, 393 | "not_operator_with_successor_space": false, 394 | "simplified_null_return": true, 395 | "nullable_type_declaration_for_default_null_value": { 396 | "use_nullable_type_declaration": true 397 | }, 398 | "assign_null_coalescing_to_coalesce_equal": true, 399 | "ternary_to_null_coalescing": true, 400 | "get_class_to_class_keyword": true 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /resources/css/tool.css: -------------------------------------------------------------------------------- 1 | @tailwind components; 2 | @tailwind utilities; 3 | -------------------------------------------------------------------------------- /resources/js/@types/index.ts: -------------------------------------------------------------------------------- 1 | export type OurFile = { 2 | id: string 3 | name: string 4 | extension: string 5 | mime: string 6 | path: string 7 | type: string 8 | url: string 9 | size: number 10 | sizeReadable?: string 11 | lastModified: number 12 | lastModifiedReadable?: string 13 | meta?: { 14 | type: string 15 | aspectRation?: string 16 | height?: number 17 | width?: 400 18 | } 19 | 20 | } 21 | 22 | export type OurFolder = { 23 | id: string 24 | name: string 25 | path: string 26 | } 27 | 28 | export type ModalPayload = any; 29 | 30 | export type Modal = { 31 | id: string 32 | name: string 33 | payload: ModalPayload, 34 | } 35 | 36 | export type OptionValue = { 37 | label: string 38 | value: string 39 | } 40 | 41 | export type Breadcrumb = { 42 | current: boolean 43 | id: string 44 | name: string 45 | path: string 46 | } 47 | 48 | export type PaginationLink = { 49 | url: string | null 50 | label: string 51 | active: boolean 52 | } 53 | 54 | export type Pagination = { 55 | current_page: number 56 | last_page: number 57 | from: number 58 | to: number 59 | total: number 60 | links: Array 61 | } 62 | 63 | export type Data = { 64 | path: string 65 | disk: string 66 | files: Array 67 | folders: Array 68 | breadcrumbs: Array 69 | filters: any 70 | pageLimits: Array 71 | pagination: Pagination 72 | } 73 | 74 | export type QueueEntryStatus = boolean | null 75 | 76 | export type QueueEntry = { 77 | id: string 78 | file: File 79 | entity?: OurFile 80 | isImage?: boolean 81 | isVideo?: boolean 82 | isArchive?: boolean 83 | ratio: number 84 | status?: QueueEntryStatus 85 | } 86 | -------------------------------------------------------------------------------- /resources/js/components/Browser.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 167 | 168 | 195 | -------------------------------------------------------------------------------- /resources/js/components/BrowserBreadcrumbs.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 52 | -------------------------------------------------------------------------------- /resources/js/components/BrowserContent.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 75 | -------------------------------------------------------------------------------- /resources/js/components/BrowserDropzone.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /resources/js/components/BrowserFile.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 181 | -------------------------------------------------------------------------------- /resources/js/components/BrowserFolder.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 69 | -------------------------------------------------------------------------------- /resources/js/components/BrowserModal.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 67 | -------------------------------------------------------------------------------- /resources/js/components/BrowserPagination.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 75 | -------------------------------------------------------------------------------- /resources/js/components/BrowserToolbar.vue: -------------------------------------------------------------------------------- 1 | 145 | 146 | 167 | -------------------------------------------------------------------------------- /resources/js/components/Elements/Button.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 25 | -------------------------------------------------------------------------------- /resources/js/components/Elements/DetailView.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 81 | 82 | 88 | -------------------------------------------------------------------------------- /resources/js/components/Elements/Dropdown.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 31 | -------------------------------------------------------------------------------- /resources/js/components/Elements/DropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/js/components/Elements/Empty.vue: -------------------------------------------------------------------------------- 1 | 8 | 27 | -------------------------------------------------------------------------------- /resources/js/components/Elements/SelectControl.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 45 | -------------------------------------------------------------------------------- /resources/js/components/Modals/BaseModal.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 94 | 95 | 102 | -------------------------------------------------------------------------------- /resources/js/components/Modals/BrowserDetailModal.vue: -------------------------------------------------------------------------------- 1 | 180 | 181 | 279 | -------------------------------------------------------------------------------- /resources/js/components/Modals/BrowserSelectedModal.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 86 | -------------------------------------------------------------------------------- /resources/js/components/Modals/CreateFolderModal.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 100 | -------------------------------------------------------------------------------- /resources/js/components/Modals/CropModal.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 86 | -------------------------------------------------------------------------------- /resources/js/components/Modals/DeleteModal.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 138 | -------------------------------------------------------------------------------- /resources/js/components/Modals/QueueModal.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 93 | -------------------------------------------------------------------------------- /resources/js/components/Modals/RenameModal.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 157 | -------------------------------------------------------------------------------- /resources/js/components/ToolbarButton.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 29 | -------------------------------------------------------------------------------- /resources/js/constants/index.ts: -------------------------------------------------------------------------------- 1 | const MODALS = { 2 | DETAIL: 'detail', 3 | SELECTED: 'selected', 4 | QUEUE: 'queue', 5 | CREATE_FOLDER: 'create-folder', 6 | CROP: 'crop', 7 | DELETE: 'delete', 8 | RENAME: 'rename', 9 | } 10 | 11 | const DELETE_STATE = { 12 | FILES: 'files', 13 | FILE: 'file', 14 | FOLDER: 'folder', 15 | } 16 | 17 | 18 | export { 19 | MODALS, 20 | DELETE_STATE, 21 | } 22 | -------------------------------------------------------------------------------- /resources/js/field/DetailField.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | 21 | -------------------------------------------------------------------------------- /resources/js/field/FormField.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /resources/js/field/IndexField.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /resources/js/helpers/csrf.ts: -------------------------------------------------------------------------------- 1 | export function csrf(): string { 2 | return (document.head.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content ?? '' 3 | } 4 | -------------------------------------------------------------------------------- /resources/js/helpers/data-transfer.ts: -------------------------------------------------------------------------------- 1 | export default async function (dataTransferItems: DataTransferItemList) { 2 | const isDirectory = (entry: FileSystemEntry): entry is FileSystemDirectoryEntry => entry.isDirectory 3 | 4 | const isFile = (entry: FileSystemEntry): entry is FileSystemFileEntry => entry.isFile 5 | 6 | const readFile = (entry: FileSystemEntry, path = ''): Promise => { 7 | return new Promise((resolve, reject) => { 8 | if (!isFile(entry)) { 9 | return 10 | } 11 | 12 | entry.file( 13 | file => resolve(new File([file], path + file.name, { type: file.type })), 14 | err => reject(err), 15 | ) 16 | }) 17 | } 18 | 19 | const dirReadEntries = (dirReader: FileSystemDirectoryReader, path: string): Promise => { 20 | return new Promise((resolve, reject) => { 21 | dirReader.readEntries( 22 | async (entries: FileSystemEntry[]) => { 23 | let files: File[] = [] 24 | 25 | for (const entry of entries) { 26 | const itemFiles = await getFilesFromEntry(entry, path) 27 | 28 | if (itemFiles !== undefined) { 29 | files = files.concat(itemFiles) 30 | } 31 | } 32 | 33 | resolve(files) 34 | }, 35 | err => reject(err), 36 | ) 37 | }) 38 | } 39 | 40 | const readDir = async (entry: FileSystemEntry, path: string) => { 41 | if (!isDirectory(entry)) { 42 | return [] 43 | } 44 | 45 | const dirReader = entry.createReader() 46 | 47 | const newPath = path + entry.name + '/' 48 | 49 | let files: File[] = [] 50 | 51 | let newFiles: File[] = [] 52 | 53 | do { 54 | newFiles = await dirReadEntries(dirReader, newPath) 55 | 56 | files = files.concat(newFiles) 57 | } while (newFiles.length > 0) 58 | 59 | return files 60 | } 61 | 62 | const getFilesFromEntry = async (entry: FileSystemEntry | null, path = '') => { 63 | if (!entry) { 64 | return undefined; // throw new Error('Entry not isFile and not isDirectory - unable to get files') 65 | } 66 | 67 | if (entry.isFile) { 68 | const file = await readFile(entry, path) 69 | 70 | return [file] 71 | } 72 | 73 | if (entry.isDirectory) { 74 | return await readDir(entry, path) 75 | } 76 | } 77 | 78 | let files: File[] = [] 79 | const entries: (FileSystemEntry | null)[] = [] 80 | 81 | const total = dataTransferItems.length 82 | 83 | for (let i = 0; i < total; i++) { 84 | entries.push(dataTransferItems[i].webkitGetAsEntry()) 85 | } 86 | 87 | for (const entry of entries) { 88 | const newFiles = await getFilesFromEntry(entry) 89 | 90 | if (newFiles !== undefined) { 91 | files = files.concat(newFiles) 92 | } 93 | } 94 | 95 | return files 96 | } 97 | -------------------------------------------------------------------------------- /resources/js/helpers/mime-icons.ts: -------------------------------------------------------------------------------- 1 | export const mimeIcons: { [key: string]: string } = { 2 | dir: "folder", 3 | dirBack: "folder-remove", 4 | audio: "music-note", 5 | image: "photograph", 6 | pdf: "document", 7 | text: "document-text", 8 | video: "video-camera", 9 | archive: 'archive', 10 | }; 11 | -------------------------------------------------------------------------------- /resources/js/helpers/transformers.ts: -------------------------------------------------------------------------------- 1 | import { OurFile } from "../@types"; 2 | 3 | export default function nativeFileToEntity(file: File) { 4 | return { 5 | id: file.name, 6 | name: file.name, 7 | extension: file.type.split('/')[1], 8 | mime: file.type, 9 | path: file.name, 10 | type: file.type.split('/')[0], 11 | url: URL.createObjectURL(file), 12 | size: Number(file.size.toString()), 13 | lastModified: Number(new Date(file.lastModified).toString()), 14 | } as OurFile 15 | } 16 | -------------------------------------------------------------------------------- /resources/js/helpers/truncate.ts: -------------------------------------------------------------------------------- 1 | 2 | export default function truncate(text: string, stop: number, clamp: string = "...") { 3 | return text.slice(0, stop) + (stop < text.length ? clamp : ""); 4 | } 5 | -------------------------------------------------------------------------------- /resources/js/package.js: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | import '../css/tool.css' 3 | 4 | import Browser from './components/Browser.vue'; 5 | import BrowserModal from './components/BrowserModal.vue'; 6 | 7 | export { 8 | Browser, 9 | BrowserModal, 10 | createPinia, 11 | } 12 | -------------------------------------------------------------------------------- /resources/js/pages/Tool.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /resources/js/tool.js: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | import '../css/tool.css' 3 | import Tool from './pages/Tool.vue' 4 | import IndexField from './field/IndexField.vue' 5 | import DetailField from './field/DetailField.vue' 6 | import FormField from './field/FormField.vue' 7 | 8 | Nova.booting((app, store) => { 9 | app.use(createPinia()) 10 | 11 | Nova.inertia('NovaFileManager', Tool) 12 | 13 | app.component('index-file-manager-field', IndexField); 14 | app.component('detail-file-manager-field', DetailField); 15 | app.component('form-file-manager-field', FormField); 16 | }) 17 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | middleware('nova')->group(static function (): void { 10 | Route::prefix('disks')->as('disks.')->group(static function (): void { 11 | Route::get('{resource?}', [DiskController::class, 'available'])->name('available'); 12 | }); 13 | 14 | Route::get('{resource?}', IndexController::class)->name('data'); 15 | 16 | Route::prefix('files')->as('files.')->group(function (): void { 17 | Route::post('upload/{resource?}', [FileController::class, 'upload'])->name('upload'); 18 | Route::post('rename/{resource?}', [FileController::class, 'rename'])->name('rename'); 19 | Route::post('delete/{resource?}', [FileController::class, 'delete'])->name('delete'); 20 | Route::post('unzip/{resource?}', [FileController::class, 'unzip'])->name('unzip'); 21 | Route::post('duplicate/{resource?}', [FileController::class, 'duplicate'])->name('duplicate'); 22 | }); 23 | 24 | Route::prefix('folders')->as('folders.')->group(function (): void { 25 | Route::post('create/{resource?}', [FolderController::class, 'create'])->name('create'); 26 | Route::post('rename/{resource?}', [FolderController::class, 'rename'])->name('rename'); 27 | Route::post('delete/{resource?}', [FolderController::class, 'delete'])->name('delete'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /routes/inertia.php: -------------------------------------------------------------------------------- 1 | name('nova-file-manager.tool'); 7 | -------------------------------------------------------------------------------- /screenshots/tool-detail-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepanenko3/nova-filemanager/9e33fbbd7db784e33648404a5ee28a57bd30effb/screenshots/tool-detail-dark.png -------------------------------------------------------------------------------- /screenshots/tool-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepanenko3/nova-filemanager/9e33fbbd7db784e33648404a5ee28a57bd30effb/screenshots/tool-detail.png -------------------------------------------------------------------------------- /screenshots/tool-inside-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepanenko3/nova-filemanager/9e33fbbd7db784e33648404a5ee28a57bd30effb/screenshots/tool-inside-dark.png -------------------------------------------------------------------------------- /screenshots/tool-inside.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepanenko3/nova-filemanager/9e33fbbd7db784e33648404a5ee28a57bd30effb/screenshots/tool-inside.png -------------------------------------------------------------------------------- /screenshots/tool-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepanenko3/nova-filemanager/9e33fbbd7db784e33648404a5ee28a57bd30effb/screenshots/tool-list.png -------------------------------------------------------------------------------- /screenshots/tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepanenko3/nova-filemanager/9e33fbbd7db784e33648404a5ee28a57bd30effb/screenshots/tool.png -------------------------------------------------------------------------------- /src/Casts/Asset.php: -------------------------------------------------------------------------------- 1 | map(fn (array $file) => new Asset(...$file)); 25 | } 26 | 27 | public function set( 28 | Model $model, 29 | string $key, 30 | $value, 31 | array $attributes, 32 | ): string { 33 | if ($value instanceof Collection) { 34 | return $value->toJson(); 35 | } 36 | 37 | throw new InvalidArgumentException('Invalid value for asset cast.'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Contracts/Entities/Entity.php: -------------------------------------------------------------------------------- 1 | data)) { 41 | if ($this->manager->filesystem()->exists($this->path)) { 42 | $this->data = [ 43 | 'id' => $this->id(), 44 | 'disk' => $this->disk, 45 | 'name' => $this->name(), 46 | 'path' => $this->path, 47 | 'size' => $this->size(), 48 | 'sizeReadable' => $this->sizeReadable(), 49 | 'extension' => $this->extension(), 50 | 'mime' => $this->mime(), 51 | 'url' => $this->url(), 52 | 'lastModifiedAt' => $this->lastModifiedAtTimestamp(), 53 | 'lastModifiedAtReadable' => $this->lastModifiedAtReadable(), 54 | 'type' => $this->type(), 55 | 'meta' => $this->meta(), 56 | ]; 57 | } else { 58 | $this->data = array_merge([ 59 | 'id' => $this->id(), 60 | 'disk' => $this->disk, 61 | 'path' => $this->path, 62 | ]); 63 | } 64 | } 65 | 66 | return $this->data; 67 | } 68 | 69 | public function id(): string 70 | { 71 | return sha1($this->manager->filesystem()->path($this->path)); 72 | } 73 | 74 | public function name(): string 75 | { 76 | return pathinfo($this->path, PATHINFO_BASENAME); 77 | } 78 | 79 | public function size(): int 80 | { 81 | return $this->manager->filesystem()->size( 82 | path: $this->path, 83 | ); 84 | } 85 | 86 | public function sizeReadable(): string 87 | { 88 | $value = $this->size(); 89 | 90 | $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; 91 | 92 | for ($i = 0; $value > 1024; $i++) { 93 | $value /= 1024; 94 | } 95 | 96 | return round($value, 2) . ' ' . $units[$i]; 97 | } 98 | 99 | public function extension(): string 100 | { 101 | return pathinfo($this->path, PATHINFO_EXTENSION); 102 | } 103 | 104 | public function mime(): string 105 | { 106 | try { 107 | $ext = pathinfo($this->path, PATHINFO_EXTENSION); 108 | $types = MimeTypes::checkMimeType($ext); 109 | 110 | $type = $types === false 111 | ? $type = $this->manager->filesystem()->mimeType($this->path) 112 | : $type = $types[0]; 113 | 114 | if ($type === false) { 115 | throw UnableToRetrieveMetadata::mimeType($this->path); 116 | } 117 | 118 | return $type; 119 | } catch (UnableToRetrieveMetadata $e) { 120 | report($e); 121 | 122 | return 'application/octet-stream'; 123 | } 124 | } 125 | 126 | public function url(): string 127 | { 128 | // if a custom url builder is defined, we use it to return the url 129 | if ($this->manager->hasUrlResolver()) { 130 | return call_user_func( 131 | $this->manager->getUrlResolver(), 132 | app(NovaRequest::class), 133 | $this->path, 134 | $this->disk, 135 | $this->manager->filesystem(), 136 | ); 137 | } 138 | 139 | $supportsSignedUrls = $this->manager->filesystem() instanceof AwsS3V3Adapter; 140 | 141 | // signed urls are only supported on S3 disks 142 | if ($supportsSignedUrls && config('nova-file-manager.url_signing.enabled')) { 143 | return $this->manager->filesystem()->temporaryUrl( 144 | $this->path, 145 | $this->signedExpirationTime(), 146 | ); 147 | } 148 | 149 | // we fallback to the regular url builder 150 | return $this->manager->filesystem()->url($this->path); 151 | } 152 | 153 | public function signedExpirationTime(): Carbon 154 | { 155 | return now()->add( 156 | unit: config('nova-file-manager.url_signing.unit'), 157 | value: config('nova-file-manager.url_signing.value'), 158 | ); 159 | } 160 | 161 | public function lastModifiedAt(): string 162 | { 163 | return $this->lastModifiedAtTimestamp()->toDateTimeString(); 164 | } 165 | 166 | public function lastModifiedAtReadable(): string 167 | { 168 | return $this->lastModifiedAtTimestamp()->diffForHumans(); 169 | } 170 | 171 | public function lastModifiedAtTimestamp(): Carbon 172 | { 173 | return Carbon::createFromTimestamp( 174 | $this->manager 175 | ->filesystem() 176 | ->lastModified($this->path), 177 | ); 178 | } 179 | 180 | public function type(): string 181 | { 182 | return get_file_type( 183 | mime: $this->mime(), 184 | ); 185 | } 186 | 187 | abstract public function meta(): array; 188 | } 189 | -------------------------------------------------------------------------------- /src/Entities/File.php: -------------------------------------------------------------------------------- 1 | 'file', 11 | ]; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Entities/Image.php: -------------------------------------------------------------------------------- 1 | manager->filesystem()->get( 12 | path: $this->path, 13 | ); 14 | 15 | try { 16 | $image = imagecreatefromstring($contents); 17 | $width = imagesx($image) ?? null; 18 | $height = imagesy($image) ?? null; 19 | 20 | return [ 21 | 'type' => 'image', 22 | 'width' => $width, 23 | 'height' => $height, 24 | 'aspectRatio' => getAspectRatio($width, $height), 25 | ]; 26 | } catch (Exception $e) { 27 | return [ 28 | 'type' => 'image', 29 | 'source' => $contents, 30 | ]; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Entities/Text.php: -------------------------------------------------------------------------------- 1 | manager->filesystem()->get( 10 | path: $this->path, 11 | ); 12 | 13 | return [ 14 | 'type' => 'text', 15 | 'source' => $contents, 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Entities/Video.php: -------------------------------------------------------------------------------- 1 | 'video', 11 | ]; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Events/FileDeleted.php: -------------------------------------------------------------------------------- 1 | prepareStorageCallback( 51 | $storageCallback 52 | ); 53 | 54 | $this->thumbnail(function (array $assets, $resource) { 55 | foreach ($assets as $asset) { 56 | if (data_get($asset, 'type') === 'image' && $url = data_get($asset, 'url')) { 57 | return $url; 58 | } 59 | } 60 | }); 61 | } 62 | 63 | public static function registerWrapper( 64 | string $name, 65 | Closure $callback 66 | ): void { 67 | static::$wrappers[$name] = $callback; 68 | } 69 | 70 | public static function forWrapper( 71 | string $name 72 | ): ?static { 73 | if (!$callback = (static::$wrappers[$name] ?? null)) { 74 | return null; 75 | } 76 | 77 | return $callback( 78 | static::make( 79 | name: 'wrapped' 80 | ) 81 | ); 82 | } 83 | 84 | public function multiple( 85 | bool $multiple = true 86 | ): static { 87 | $this->multiple = $multiple; 88 | 89 | return $this; 90 | } 91 | 92 | public function limit( 93 | ?int $limit = null 94 | ): static { 95 | $this->limit = $limit; 96 | 97 | return $this; 98 | } 99 | 100 | public function asHtml(): static 101 | { 102 | $this->asHtml = true; 103 | 104 | return $this; 105 | } 106 | 107 | public function wrapper( 108 | string $name 109 | ): static { 110 | $this->wrapper = $name; 111 | 112 | return $this; 113 | } 114 | 115 | public function resolveThumbnailUrl() 116 | { 117 | return is_callable($this->thumbnailUrlCallback) && !empty($this->value) 118 | ? call_user_func($this->thumbnailUrlCallback, $this->value, $this->resource) 119 | : null; 120 | } 121 | 122 | public function applyWrapper(): static 123 | { 124 | if (empty($this->wrapper)) { 125 | return $this; 126 | } 127 | 128 | if (!$wrapper = static::forWrapper($this->wrapper)) { 129 | return $this; 130 | } 131 | 132 | $this->prepareStorageCallback( 133 | $wrapper->storageCallback 134 | ); 135 | 136 | $this->multiple = $wrapper->multiple; 137 | $this->limit = $wrapper->limit; 138 | $this->asHtml = $wrapper->asHtml; 139 | 140 | $this->merge($wrapper); 141 | 142 | return $this; 143 | } 144 | 145 | public function jsonSerialize(): array 146 | { 147 | $this->applyWrapper(); 148 | 149 | return array_merge( 150 | parent::jsonSerialize(), 151 | [ 152 | 'multiple' => $this->multiple, 153 | 'limit' => $this->multiple ? $this->limit : 1, 154 | 'asHtml' => $this->asHtml, 155 | 'wrapper' => $this->wrapper, 156 | ], 157 | $this->options(), 158 | ); 159 | } 160 | 161 | protected function fillAttribute( 162 | NovaRequest $request, 163 | $requestAttribute, 164 | $model, 165 | $attribute, 166 | ) { 167 | $this->applyWrapper(); 168 | 169 | $result = call_user_func( 170 | $this->storageCallback, 171 | $request, 172 | $model, 173 | $attribute, 174 | $requestAttribute 175 | ); 176 | 177 | if ($result === true) { 178 | return; 179 | } 180 | 181 | if ($result instanceof Closure) { 182 | return $result; 183 | } 184 | 185 | if (!is_array($result)) { 186 | return $model->{$attribute} = $result; 187 | } 188 | 189 | foreach ($result as $key => $value) { 190 | $model->{$key} = $value; 191 | } 192 | } 193 | 194 | protected function prepareStorageCallback( 195 | ?Closure $storageCallback = null 196 | ): void { 197 | $this->storageCallback = $storageCallback ?? function ( 198 | NovaRequest $request, 199 | $model, 200 | string $attribute, 201 | string $requestAttribute 202 | ) { 203 | $value = $request->input($requestAttribute); 204 | 205 | try { 206 | $payload = json_decode($value ?? '', true, 512, JSON_THROW_ON_ERROR); 207 | } catch (JsonException) { 208 | $payload = []; 209 | } 210 | 211 | $files = collect($payload); 212 | 213 | if ($this->multiple) { 214 | $value = collect($files) 215 | ->map(fn (array $file) => new Asset(...$file)); 216 | } else { 217 | $value = $files->isNotEmpty() 218 | ? new Asset(...$files->first()) 219 | : null; 220 | } 221 | 222 | return [$attribute => $value]; 223 | }; 224 | } 225 | 226 | protected function resolveAttribute( 227 | $resource, 228 | $attribute = null 229 | ): ?array { 230 | if (!$value = parent::resolveAttribute( 231 | $resource, 232 | $attribute 233 | )) { 234 | return null; 235 | } 236 | 237 | if ($value instanceof Asset) { 238 | $value = collect([$value]); 239 | } 240 | 241 | if ($value instanceof stdClass) { 242 | $value = (array) $value; 243 | } 244 | 245 | if (is_array($value)) { 246 | if ($this->multiple) { 247 | $value = collect($value) 248 | ->map( 249 | fn (array | object $asset) => new Asset( 250 | ...(array) $asset 251 | ) 252 | ); 253 | } else { 254 | $value = collect([ 255 | new Asset( 256 | ...$value 257 | ), 258 | ]); 259 | } 260 | } 261 | 262 | $this->applyWrapper(); 263 | 264 | return $value 265 | ->map(function (Asset $asset) { 266 | $disk = $this->resolveFilesystem(app(NovaRequest::class)) ?? $asset->disk; 267 | 268 | $manager = app( 269 | FileManagerContract::class, 270 | [ 271 | 'disk' => $disk, 272 | ] 273 | ); 274 | 275 | if ($this->hasUrlResolver()) { 276 | $manager->resolveUrlUsing( 277 | $this->getUrlResolver() 278 | ); 279 | } 280 | 281 | return $manager->makeEntity( 282 | $asset->path, 283 | $asset->disk 284 | ); 285 | }) 286 | ->toArray(); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/FileManagerTool.php: -------------------------------------------------------------------------------- 1 | path( 27 | '/' . ltrim(config('nova-file-manager.path', 'filemanager'), '/') 28 | ) 29 | ->icon('folder'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Filesystem/Upload/Uploader.php: -------------------------------------------------------------------------------- 1 | validateUpload()) { 22 | throw ValidationException::withMessages(['file' => [__('nova-file-manager::errors.file.upload_validation')]]); 23 | } 24 | 25 | $receiver = new FileReceiver( 26 | fileIndexOrFile: $index, 27 | request: $request, 28 | handlerClass: HandlerFactory::classFromRequest($request) 29 | ); 30 | 31 | if ($receiver->isUploaded() === false) { 32 | throw new UploadMissingFileException(); 33 | } 34 | 35 | $save = $receiver->receive(); 36 | 37 | if ($save->isFinished()) { 38 | return $this->saveFile( 39 | request: $request, 40 | file: $save->getFile(), 41 | ); 42 | } 43 | 44 | $handler = $save->handler(); 45 | 46 | return [ 47 | 'done' => $handler->getPercentageDone(), 48 | 'status' => true, 49 | ]; 50 | } 51 | 52 | public function saveFile( 53 | UploadFileRequest $request, 54 | UploadedFile $file, 55 | ): array { 56 | if (!$request->validateUpload($file, true)) { 57 | throw ValidationException::withMessages(['file' => [__('nova-file-manager::errors.file.upload_validation')]]); 58 | } 59 | 60 | $folderPath = dirname($request->filePath()); 61 | $filePath = $file->getClientOriginalName(); 62 | $testPath = ltrim(str_replace('//', '/', "{$folderPath}/{$filePath}"), '/'); 63 | 64 | event( 65 | new FileUploading( 66 | filesystem: $request->manager()->filesystem(), 67 | disk: $request->manager()->getDisk(), 68 | path: $testPath, 69 | ), 70 | ); 71 | 72 | $path = $request->manager()->filesystem()->putFileAs( 73 | path: $folderPath, 74 | file: $file, 75 | name: $filePath, 76 | ); 77 | 78 | event( 79 | new FileUploaded( 80 | filesystem: $request->manager()->filesystem(), 81 | disk: $request->manager()->getDisk(), 82 | path: $path, 83 | ), 84 | ); 85 | 86 | return [ 87 | 'message' => __('nova-file-manager::messages.file.upload'), 88 | ]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Http/Controllers/DiskController.php: -------------------------------------------------------------------------------- 1 | json( 13 | config('nova-file-manager.available_disks', []), 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Http/Controllers/FileController.php: -------------------------------------------------------------------------------- 1 | json( 30 | $uploader->handle($request) 31 | ); 32 | } 33 | 34 | public function rename( 35 | RenameFileRequest $request, 36 | ): JsonResponse { 37 | $manager = $request->manager(); 38 | 39 | event(new FileRenaming( 40 | filesystem: $manager->filesystem(), 41 | disk: $manager->getDisk(), 42 | from: $request->from, 43 | to: $request->to, 44 | )); 45 | 46 | $result = $manager->rename( 47 | from: $request->from, 48 | to: $request->to, 49 | ); 50 | 51 | if (!$result) { 52 | throw ValidationException::withMessages(['from' => [__('nova-file-manager::errors.file.rename')]]); 53 | } 54 | 55 | event(new FileRenamed( 56 | filesystem: $manager->filesystem(), 57 | disk: $manager->getDisk(), 58 | from: $request->from, 59 | to: $request->to, 60 | )); 61 | 62 | return response()->json([ 63 | 'message' => __('nova-file-manager::messages.file.rename'), 64 | ]); 65 | } 66 | 67 | public function delete( 68 | DeleteFileRequest $request, 69 | ): JsonResponse { 70 | $manager = $request->manager(); 71 | 72 | foreach ($request->paths as $path) { 73 | event(new FileDeleting( 74 | filesystem: $manager->filesystem(), 75 | disk: $manager->getDisk(), 76 | path: $path, 77 | )); 78 | 79 | $result = $manager->delete( 80 | path: $path, 81 | ); 82 | 83 | if (!$result) { 84 | throw ValidationException::withMessages(['paths' => [__('nova-file-manager::errors.file.delete')]]); 85 | } 86 | 87 | event(new FileDeleted( 88 | filesystem: $manager->filesystem(), 89 | disk: $manager->getDisk(), 90 | path: $path, 91 | )); 92 | } 93 | 94 | return response()->json([ 95 | 'message' => __('nova-file-manager::messages.file.delete'), 96 | ]); 97 | } 98 | 99 | public function duplicate( 100 | DuplicateFileRequest $request, 101 | ): JsonResponse { 102 | $manager = $request->manager(); 103 | 104 | event(new FileDuplicating( 105 | filesystem: $manager->filesystem(), 106 | disk: $manager->getDisk(), 107 | path: $request->path, 108 | )); 109 | 110 | $result = $manager->duplicate( 111 | path: $request->path, 112 | ); 113 | 114 | if (!$result) { 115 | throw ValidationException::withMessages(['path' => [__('nova-file-manager::errors.file.duplicate')]]); 116 | } 117 | 118 | event(new FileDuplicated( 119 | filesystem: $manager->filesystem(), 120 | disk: $manager->getDisk(), 121 | path: $request->path, 122 | )); 123 | 124 | return response()->json([ 125 | 'message' => __('nova-file-manager::messages.file.duplicate'), 126 | ]); 127 | } 128 | 129 | public function unzip( 130 | UnzipFileRequest $request, 131 | ): JsonResponse { 132 | $manager = $request->manager(); 133 | 134 | event(new FileUnzipping( 135 | filesystem: $manager->filesystem(), 136 | disk: $manager->getDisk(), 137 | path: $request->path, 138 | )); 139 | 140 | $result = $manager->unzip( 141 | path: $request->path, 142 | ); 143 | 144 | if (!$result) { 145 | throw ValidationException::withMessages(['path' => [__('nova-file-manager::errors.file.unzip')]]); 146 | } 147 | 148 | event(new FileUnzipped( 149 | filesystem: $manager->filesystem(), 150 | disk: $manager->getDisk(), 151 | path: $request->path, 152 | )); 153 | 154 | return response()->json([ 155 | 'message' => __('nova-file-manager::messages.file.unzip'), 156 | ]); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Http/Controllers/FolderController.php: -------------------------------------------------------------------------------- 1 | path); 24 | 25 | event(new FolderCreating( 26 | filesystem: $request->manager()->filesystem(), 27 | disk: $request->manager()->getDisk(), 28 | path: $path, 29 | )); 30 | 31 | $result = $request->manager()->mkdir( 32 | path: $path, 33 | ); 34 | 35 | if (!$result) { 36 | throw ValidationException::withMessages(['folder' => [__('nova-file-manager::errors.folder.create')]]); 37 | } 38 | 39 | event(new FolderCreated( 40 | filesystem: $request->manager()->filesystem(), 41 | disk: $request->manager()->getDisk(), 42 | path: $path, 43 | )); 44 | 45 | return response()->json([ 46 | 'message' => __('nova-file-manager::messages.folder.create'), 47 | ]); 48 | } 49 | 50 | public function rename( 51 | RenameFolderRequest $request, 52 | ): JsonResponse { 53 | event(new FolderRenaming( 54 | filesystem: $request->manager()->filesystem(), 55 | disk: $request->manager()->getDisk(), 56 | from: $request->from, 57 | to: $request->to, 58 | )); 59 | 60 | $result = $request->manager()->rename( 61 | from: $request->from, 62 | to: $request->to, 63 | ); 64 | 65 | if (!$result) { 66 | throw ValidationException::withMessages(['folder' => [__('nova-file-manager::errors.folder.rename')]]); 67 | } 68 | 69 | event(new FolderRenamed( 70 | filesystem: $request->manager()->filesystem(), 71 | disk: $request->manager()->getDisk(), 72 | from: $request->from, 73 | to: $request->to, 74 | )); 75 | 76 | return response()->json([ 77 | 'message' => __('nova-file-manager::messages.folder.rename'), 78 | ]); 79 | } 80 | 81 | public function delete( 82 | DeleteFolderRequest $request, 83 | ): JsonResponse { 84 | event(new FolderDeleting( 85 | filesystem: $request->manager()->filesystem(), 86 | disk: $request->manager()->getDisk(), 87 | path: $request->path, 88 | )); 89 | 90 | $result = $request->manager()->rmdir( 91 | path: $request->path, 92 | ); 93 | 94 | if (!$result) { 95 | throw ValidationException::withMessages(['folder' => [__('nova-file-manager::errors.folder.delete')]]); 96 | } 97 | 98 | event(new FolderDeleted( 99 | filesystem: $request->manager()->filesystem(), 100 | disk: $request->manager()->getDisk(), 101 | path: $request->path, 102 | )); 103 | 104 | return response()->json([ 105 | 'message' => __('nova-file-manager::messages.folder.delete'), 106 | ]); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Http/Controllers/IndexController.php: -------------------------------------------------------------------------------- 1 | manager(); 14 | 15 | $paginator = $manager 16 | ->paginate($manager->files()); 17 | 18 | return response()->json([ 19 | 'path' => $manager->getPath(), 20 | 'disk' => $manager->getDisk(), 21 | 'breadcrumbs' => $manager->breadcrumbs(), 22 | 'folders' => $manager->directories(), 23 | 'files' => $paginator->items(), 24 | 'pagination' => [ 25 | 'current_page' => $paginator->currentPage(), 26 | 'last_page' => $paginator->lastPage(), 27 | 'from' => $paginator->firstItem(), 28 | 'to' => $paginator->lastItem(), 29 | 'total' => $paginator->total(), 30 | 'links' => $paginator->linkCollection()->toArray(), 31 | ], 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Controllers/ToolController.php: -------------------------------------------------------------------------------- 1 | first( 26 | fn (Tool $tool) => $tool instanceof FileManagerTool, 27 | ); 28 | 29 | return Inertia::render( 30 | component: 'NovaFileManager', 31 | props: [ 32 | 'config' => array_merge( 33 | [ 34 | 'upload' => config('nova-file-manager.upload'), 35 | 'outdated' => $this->updateChecker(), 36 | ], 37 | $tool?->options(), 38 | ), 39 | ], 40 | ); 41 | } 42 | 43 | public function updateChecker(): Closure 44 | { 45 | return function () { 46 | if (!config('nova-file-manager.update_checker.enabled')) { 47 | return false; 48 | } 49 | 50 | return Cache::remember( 51 | key: 'nova-file-manager.update_checker', 52 | ttl: (int) CarbonInterval::days(config('nova-file-manager.update_checker.ttl_in_days'))->totalSeconds, 53 | callback: function () { 54 | $current = InstalledVersions::getPrettyVersion( 55 | packageName: 'stepanenko3/nova-filemanager', 56 | ); 57 | $latest = Http::get('https://api.github.com/repos/stepanenko3/nova-filemanager/releases/latest') 58 | ->json('tag_name'); 59 | 60 | return version_compare($current, $latest, '<'); 61 | } 62 | ); 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Http/Middleware/Authorize.php: -------------------------------------------------------------------------------- 1 | first([ 21 | $this, 22 | 'matchesTool', 23 | ]); 24 | 25 | return optional($tool)->authorize($request) 26 | ? $next($request) 27 | : abort(403); 28 | } 29 | 30 | public function matchesTool( 31 | Tool $tool, 32 | ): bool { 33 | return $tool instanceof FileManagerTool; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Http/Requests/BaseRequest.php: -------------------------------------------------------------------------------- 1 | element(); 23 | 24 | /** @var \Stepanenko3\NovaFileManager\Services\FileManagerService $manager */ 25 | $manager = app( 26 | abstract: FileManagerContract::class, 27 | parameters: $element?->hasCustomFilesystem() 28 | ? [ 29 | 'disk' => $element?->resolveFilesystem( 30 | request: $this, 31 | ), 32 | ] : [], 33 | ); 34 | 35 | if ($element?->hasUrlResolver()) { 36 | $manager->resolveUrlUsing( 37 | resolver: $element?->getUrlResolver(), 38 | ); 39 | } 40 | 41 | return $manager; 42 | }); 43 | } 44 | 45 | public function element(): ?InteractsWithFilesystem 46 | { 47 | return filter_var($this->fieldMode, FILTER_VALIDATE_BOOL) 48 | ? $this->resolveField() 49 | : $this->resolveTool(); 50 | } 51 | 52 | public function resolveField(): ?InteractsWithFilesystem 53 | { 54 | if (!empty($this->wrapper) && $field = FileManager::forWrapper($this->wrapper)) { 55 | return $field; 56 | } 57 | 58 | $resource = !(empty($this->resourceId)) 59 | ? $this->findResourceOrFail() 60 | : $this->newResource(); 61 | 62 | $fields = $this->has('flexible') 63 | ? $this->flexibleAvailableFields( 64 | resource: $resource, 65 | ) 66 | : $resource->availableFields( 67 | request: $this, 68 | ); 69 | 70 | return $fields 71 | ->whereInstanceOf(FileManager::class) 72 | ->findFieldByAttribute( 73 | attribute: $this->attribute, 74 | default: function (): void { 75 | abort(404); 76 | }, 77 | ); 78 | } 79 | 80 | public function flexibleAvailableFields( 81 | Resource $resource, 82 | ): FieldCollection { 83 | $path = $this->input( 84 | key: 'flexible', 85 | ); 86 | 87 | abort_if(blank($path), 404); 88 | 89 | $tree = collect(explode('.', $path)) 90 | ->map( 91 | function (string $item) { 92 | [$layout, $attribute] = explode(':', $item); 93 | 94 | return [ 95 | 'attribute' => $attribute, 96 | 'layout' => $layout, 97 | ]; 98 | } 99 | ); 100 | 101 | $fields = $resource->availableFields( 102 | request: $this, 103 | ); 104 | 105 | while ($tree->isNotEmpty()) { 106 | $current = $tree->shift(); 107 | 108 | $fields = $this->flexibleFieldCollection( 109 | fields: $fields, 110 | attribute: $current['attribute'], 111 | name: $current['layout'], 112 | ); 113 | } 114 | 115 | return $fields; 116 | } 117 | 118 | public function flexibleFieldCollection( 119 | FieldCollection $fields, 120 | string $attribute, 121 | string $name, 122 | ): FieldCollection { 123 | /** @var \Whitecube\NovaFlexibleContent\Flexible $field */ 124 | $field = $fields 125 | ->whereInstanceOf( 126 | type: 'Whitecube\NovaFlexibleContent\Flexible', 127 | ) 128 | ->findFieldByAttribute( 129 | attribute: $attribute, 130 | default: function (): void { 131 | abort(404); 132 | }, 133 | ); 134 | 135 | // @var \Whitecube\NovaFlexibleContent\Layouts\Collection $layouts 136 | abort_unless( 137 | boolean: $layouts = $field?->layouts, 138 | code: 404, 139 | ); 140 | 141 | /** @var \Whitecube\NovaFlexibleContent\Layouts\Layout $layout */ 142 | $layout = $layouts->first( 143 | fn ($layout) => $layout->name() === $name, 144 | ); 145 | 146 | abort_if( 147 | boolean: $layout === null, 148 | code: 404, 149 | ); 150 | 151 | return new FieldCollection( 152 | items: $layout->fields(), 153 | ); 154 | } 155 | 156 | public function resolveTool(): ?InteractsWithFilesystem 157 | { 158 | return tap( 159 | value: once(fn () => collect(Nova::registeredTools()) 160 | ->first( 161 | fn (Tool $tool) => $tool instanceof FileManagerTool, 162 | )), 163 | callback: function (?FileManagerTool $tool): void { 164 | abort_if( 165 | boolean: null === $tool, 166 | code: 404, 167 | ); 168 | } 169 | ); 170 | } 171 | 172 | public function canCreateFolder(): bool 173 | { 174 | return $this->element()?->resolveCanCreateFolder($this) ?? true; 175 | } 176 | 177 | public function canRenameFolder(): bool 178 | { 179 | return $this->element()?->resolveCanRenameFolder($this) ?? true; 180 | } 181 | 182 | public function canDeleteFolder(): bool 183 | { 184 | return $this->element()?->resolveCanDeleteFolder($this) ?? true; 185 | } 186 | 187 | public function canUploadFile(): bool 188 | { 189 | return $this->element()?->resolveCanUploadFile($this) ?? true; 190 | } 191 | 192 | public function canRenameFile(): bool 193 | { 194 | return $this->element()?->resolveCanRenameFile($this) ?? true; 195 | } 196 | 197 | public function canDuplicateFile(): bool 198 | { 199 | return $this->element()?->resolveCanDuplicateFile($this) ?? true; 200 | } 201 | 202 | public function canDeleteFile(): bool 203 | { 204 | return $this->element()?->resolveCanDeleteFile($this) ?? true; 205 | } 206 | 207 | public function canUnzipArchive(): bool 208 | { 209 | return $this->element()?->resolveCanUnzipFile($this) ?? true; 210 | } 211 | 212 | public function authorizationAttribute(): string 213 | { 214 | return strtolower(str(static::class)->classBasename()->ucsplit()->get(1, '')); 215 | } 216 | 217 | public function authorizationActionAttribute( 218 | ?string $class = null, 219 | ): string { 220 | return (string) str($class ?? static::class) 221 | ->classBasename() 222 | ->replace('Request', '') 223 | ->snake(' '); 224 | } 225 | 226 | protected function failedAuthorization(): void 227 | { 228 | throw ValidationException::withMessages([$this->authorizationAttribute() => __(key: 'nova-file-manager::errors.authorization.unauthorized', replace: ['action' => $this->authorizationActionAttribute()])]); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/Http/Requests/CreateFolderRequest.php: -------------------------------------------------------------------------------- 1 | canCreateFolder(); 13 | } 14 | 15 | public function rules(): array 16 | { 17 | return [ 18 | 'disk' => [ 19 | 'sometimes', 20 | 'string', 21 | new DiskExistsRule(), 22 | ], 23 | 'path' => [ 24 | 'required', 25 | 'string', 26 | 'min:1', 27 | new MissingInFilesystem( 28 | request: $this, 29 | ), 30 | ], 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Http/Requests/DeleteFileRequest.php: -------------------------------------------------------------------------------- 1 | canDeleteFile(); 13 | } 14 | 15 | public function rules(): array 16 | { 17 | return [ 18 | 'disk' => [ 19 | 'sometimes', 20 | 'string', 21 | new DiskExistsRule(), 22 | ], 23 | 'paths' => [ 24 | 'required', 25 | 'array', 26 | ], 27 | 'paths.*' => [ 28 | 'required', 29 | 'string', 30 | new ExistsInFilesystem( 31 | request: $this, 32 | ), 33 | ], 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Http/Requests/DeleteFolderRequest.php: -------------------------------------------------------------------------------- 1 | canDeleteFolder(); 13 | } 14 | 15 | public function rules(): array 16 | { 17 | return [ 18 | 'disk' => [ 19 | 'sometimes', 20 | 'string', 21 | new DiskExistsRule(), 22 | ], 23 | 'path' => [ 24 | 'sometimes', 25 | 'string', 26 | new ExistsInFilesystem( 27 | request: $this, 28 | ), 29 | ], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Http/Requests/DuplicateFileRequest.php: -------------------------------------------------------------------------------- 1 | canDuplicateFile(); 13 | } 14 | 15 | public function rules(): array 16 | { 17 | return [ 18 | 'disk' => [ 19 | 'sometimes', 20 | 'string', 21 | new DiskExistsRule(), 22 | ], 23 | 'path' => [ 24 | 'required', 25 | 'string', 26 | new ExistsInFilesystem( 27 | request: $this, 28 | ), 29 | ], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Http/Requests/IndexRequest.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'sometimes', 15 | 'string', 16 | new DiskExistsRule(), 17 | ], 18 | 'path' => [ 19 | 'sometimes', 20 | 'string', 21 | new ExistsInFilesystem($this), 22 | ], 23 | 'page' => [ 24 | 'sometimes', 25 | 'numeric', 26 | 'min:1', 27 | ], 28 | 'perPage' => [ 29 | 'sometimes', 30 | 'numeric', 31 | 'min:1', 32 | ], 33 | 'search' => [ 34 | 'nullable', 35 | 'string', 36 | ], 37 | 'period' => [ 38 | 'nullable', 39 | 'string', 40 | ], 41 | 'sort' => [ 42 | 'nullable', 43 | 'string', 44 | 'in:date,date-desc,name,name-desc,size,size-desc', 45 | ], 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Http/Requests/RenameFileRequest.php: -------------------------------------------------------------------------------- 1 | canRenameFile(); 14 | } 15 | 16 | public function rules(): array 17 | { 18 | return [ 19 | 'disk' => [ 20 | 'sometimes', 21 | 'string', 22 | new DiskExistsRule(), 23 | ], 24 | 'from' => [ 25 | 'required', 26 | 'string', 27 | new ExistsInFilesystem($this), 28 | ], 29 | 'to' => [ 30 | 'required', 31 | 'string', 32 | new MissingInFilesystem($this), 33 | ], 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Http/Requests/RenameFolderRequest.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'sometimes', 16 | 'string', 17 | new DiskExistsRule(), 18 | ], 19 | 'from' => [ 20 | 'required', 21 | 'string', 22 | new ExistsInFilesystem( 23 | request: $this, 24 | ), 25 | ], 26 | 'to' => [ 27 | 'required', 28 | 'string', 29 | new MissingInFilesystem( 30 | request: $this, 31 | ), 32 | ], 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Http/Requests/UnzipFileRequest.php: -------------------------------------------------------------------------------- 1 | canUnzipArchive(); 13 | } 14 | 15 | public function rules(): array 16 | { 17 | return [ 18 | 'disk' => [ 19 | 'sometimes', 20 | 'string', 21 | new DiskExistsRule(), 22 | ], 23 | 'path' => [ 24 | 'required', 25 | 'string', 26 | new ExistsInFilesystem( 27 | request: $this, 28 | ), 29 | ], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Http/Requests/UploadFileRequest.php: -------------------------------------------------------------------------------- 1 | canUploadFile()) { 15 | return false; 16 | } 17 | 18 | $path = ltrim( 19 | string: dirname( 20 | path: $this->input('resumableFilename'), 21 | ), 22 | characters: '/.' 23 | ); 24 | 25 | if (!empty($path) && !$this->canCreateFolder()) { 26 | return false; 27 | } 28 | 29 | return true; 30 | } 31 | 32 | public function authorizationActionAttribute( 33 | ?string $class = null, 34 | ): string { 35 | if (!$this->canUploadFile()) { 36 | return parent::authorizationActionAttribute(); 37 | } 38 | 39 | return parent::authorizationActionAttribute( 40 | class: CreateFolderRequest::class, 41 | ); 42 | } 43 | 44 | public function rules(): array 45 | { 46 | return [ 47 | 'disk' => [ 48 | 'sometimes', 49 | 'string', 50 | new DiskExistsRule(), 51 | ], 52 | 'path' => [ 53 | 'required', 54 | 'string', 55 | ], 56 | 'file' => array_merge( 57 | [ 58 | 'required', 59 | 'file', 60 | new FileMissingInFilesystem( 61 | request: $this, 62 | ), 63 | ], 64 | $this->element()->getUploadRules(), 65 | ), 66 | ]; 67 | } 68 | 69 | public function validateUpload( 70 | ?UploadedFile $file = null, 71 | bool $saving = false, 72 | ): bool { 73 | if (!$this->element()->hasUploadValidator()) { 74 | return true; 75 | } 76 | 77 | $file ??= $this->file('file'); 78 | 79 | return call_user_func( 80 | $this->element()->getUploadValidator(), 81 | $this, 82 | $file, 83 | $saving, 84 | ); 85 | } 86 | 87 | public function filePath(): string 88 | { 89 | $path = implode( 90 | separator: '/', 91 | array: array_filter([ 92 | Str::finish( 93 | value: $this->path, 94 | cap: '/', 95 | ), 96 | ltrim( 97 | string: $this->input('resumableFilename'), 98 | characters: '/', 99 | ), 100 | ]) 101 | ); 102 | 103 | return str_replace('//', '/', $path); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Rules/DiskExistsRule.php: -------------------------------------------------------------------------------- 1 | path = $value; 23 | 24 | return empty($value) 25 | || $value === '/' 26 | || $this->request 27 | ->manager() 28 | ->filesystem() 29 | ->exists($value); 30 | } 31 | 32 | public function message(): string 33 | { 34 | return __( 35 | 'nova-file-manager::validation.path.missing', 36 | [ 37 | 'path' => $this->path, 38 | ], 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Rules/FileLimit.php: -------------------------------------------------------------------------------- 1 | attribute = $attribute; 23 | 24 | $value = collect( 25 | json_decode( 26 | json: $value, 27 | associative: true, 28 | depth: 512, 29 | flags: JSON_THROW_ON_ERROR 30 | ), 31 | ); 32 | 33 | $total = $value->count(); 34 | 35 | return max($this->min, 0) <= $total && max($this->max, 0) >= $total; 36 | } 37 | 38 | public function message(): string 39 | { 40 | return __( 41 | 'validation.between.array', 42 | [ 43 | 'attribute' => $this->attribute, 44 | 'min' => $this->min, 45 | 'max' => $this->max, 46 | ] 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Rules/FileMissingInFilesystem.php: -------------------------------------------------------------------------------- 1 | request 21 | ->manager() 22 | ->filesystem() 23 | ->missing( 24 | $this->request->filePath(), 25 | ); 26 | } 27 | 28 | public function message(): string 29 | { 30 | return __( 31 | 'nova-file-manager::validation.path.exists', 32 | [ 33 | 'path' => $this->request->filePath(), 34 | ], 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Rules/MissingInFilesystem.php: -------------------------------------------------------------------------------- 1 | path = $value; 23 | 24 | return $this->request 25 | ->manager() 26 | ->filesystem() 27 | ->missing($value); 28 | } 29 | 30 | public function message(): string 31 | { 32 | return __( 33 | 'nova-file-manager::validation.path.exists', 34 | [ 35 | 'path' => $this->path, 36 | ] 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Support/Asset.php: -------------------------------------------------------------------------------- 1 | forwardCallTo( 27 | object: $this->filesystem(), 28 | method: $name, 29 | parameters: $arguments 30 | ); 31 | } 32 | 33 | public function __toString(): string 34 | { 35 | return json_encode( 36 | $this->jsonSerialize(), 37 | JSON_THROW_ON_ERROR, 38 | ); 39 | } 40 | 41 | public function filesystem(): Filesystem 42 | { 43 | if (!$this->filesystem) { 44 | $this->filesystem = Storage::disk($this->disk); 45 | } 46 | 47 | return $this->filesystem; 48 | } 49 | 50 | public function toArray(): array 51 | { 52 | return [ 53 | 'disk' => $this->disk, 54 | 'path' => $this->path, 55 | ]; 56 | } 57 | 58 | public function jsonSerialize(): array 59 | { 60 | return $this->toArray(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/ToolServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadTranslationsFrom( 21 | path: __DIR__ . '/../lang', 22 | namespace: 'nova-filemanager', 23 | ); 24 | 25 | $this->loadJsonTranslationsFrom( 26 | path: __DIR__ . '/../lang', 27 | ); 28 | 29 | $this->mergeConfigFrom( 30 | __DIR__ . '/../config/nova-file-manager.php', 31 | 'nova-file-manager', 32 | ); 33 | 34 | $this->publishes( 35 | [ 36 | __DIR__ . '/../config' => config_path(), 37 | ], 38 | 'nova-file-manager-config' 39 | ); 40 | 41 | $this->app->booted(function (): void { 42 | $this->routes(); 43 | }); 44 | 45 | Nova::serving(function (): void { 46 | Nova::style( 47 | 'nova-file-manager', 48 | __DIR__ . '/../dist/css/tool.css' 49 | ); 50 | Nova::script( 51 | 'nova-file-manager', 52 | __DIR__ . '/../dist/js/tool.js' 53 | ); 54 | 55 | Nova::translations(__DIR__ . '/../lang/' . app()->getLocale() . '.json'); 56 | }); 57 | } 58 | 59 | public function register(): void 60 | { 61 | $this->app->singleton( 62 | abstract: UploaderContract::class, 63 | concrete: Uploader::class, 64 | ); 65 | 66 | $this->app->singleton( 67 | abstract: FileManagerContract::class, 68 | concrete: function (Application $app, array $args = []) { 69 | /** @var \Illuminate\Http\Request $request */ 70 | $request = $app->make('request'); 71 | 72 | $disk = $args['disk'] ?? $request->input('disk'); 73 | $path = $args['path'] ?? $request->input('path', \DIRECTORY_SEPARATOR); 74 | $page = (int) ($args['page'] ?? $request->input('page', 1)); 75 | $perPage = (int) ($args['perPage'] ?? $request->input('perPage', 15)); 76 | $search = $args['search'] ?? $request->input('search'); 77 | $filter = $args['filter'] ?? $request->input('filter'); 78 | $sort = $args['sort'] ?? $request->input('sort'); 79 | $period = $args['period'] ?? $request->input('period'); 80 | 81 | return FileManagerService::make( 82 | disk: $disk, 83 | path: $path, 84 | page: $page, 85 | perPage: $perPage, 86 | search: $search, 87 | filter: $filter, 88 | sort: $sort, 89 | period: $period, 90 | ); 91 | } 92 | ); 93 | } 94 | 95 | protected function routes(): void 96 | { 97 | if ($this->app->routesAreCached()) { 98 | return; 99 | } 100 | 101 | Nova::router( 102 | [ 103 | 'nova', 104 | Authenticate::class, 105 | Authorize::class, 106 | ], 107 | config('nova-file-manager.path', 'file-manager'), 108 | ) 109 | ->group(__DIR__ . '/../routes/inertia.php'); 110 | 111 | Route::middleware([ 112 | 'nova:api', 113 | Authorize::class, 114 | ]) 115 | ->prefix('nova-vendor/nova-file-manager') 116 | ->group(__DIR__ . '/../routes/api.php'); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Traits/Support/ResolvesUrl.php: -------------------------------------------------------------------------------- 1 | urlResolver ?? null; 14 | } 15 | 16 | public function hasUrlResolver(): bool 17 | { 18 | return isset($this->urlResolver) && is_callable($this->urlResolver); 19 | } 20 | 21 | public function resolveUrlUsing( 22 | Closure $resolver, 23 | ): static { 24 | $this->urlResolver = $resolver; 25 | 26 | return $this; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | $contents) { 12 | if (Str::contains($mime, $contents)) { 13 | return $type; 14 | } 15 | } 16 | 17 | return 'file'; 18 | } 19 | } 20 | if (!function_exists('str')) { 21 | function str(?string $string = null): Stringable | string 22 | { 23 | if (func_num_args() === 0) { 24 | return new class () { 25 | public function __call($method, $parameters) 26 | { 27 | return Str::$method(...$parameters); 28 | } 29 | 30 | public function __toString() 31 | { 32 | return ''; 33 | } 34 | }; 35 | } 36 | 37 | return Str::of($string); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./resources/js/**/*.{vue,js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | important: '.nova-file-manager', 8 | plugins: [ 9 | require('tailwind-scrollbar-hide'), 10 | ], 11 | darkMode: 'class' 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "module": "ES2022", 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["ESNext", "DOM"], 14 | "skipLibCheck": true, 15 | "noEmit": true, 16 | "outDir": "dist", 17 | "baseUrl": "resources/js" 18 | }, 19 | "include": [ 20 | "resources/**/*.ts", 21 | "resources/**/*.d.ts", 22 | "resources/**/*.tsx", 23 | "resources/**/*.vue", 24 | "resources/js/package.js" 25 | ], 26 | "references": [ 27 | { 28 | "path": "./tsconfig.node.json" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { resolve } from 'path' 3 | import { defineConfig } from 'vite' 4 | 5 | const config = { 6 | tool: { 7 | entry: resolve(__dirname, 'resources/js/tool.js'), 8 | name: 'tool', 9 | fileName: () => 'js/tool.js', 10 | formats: ["umd"], 11 | }, 12 | package: { 13 | entry: resolve(__dirname, 'resources/js/package.js'), 14 | name: 'package', 15 | fileName: () => 'js/package.js', 16 | formats: ["es"], 17 | } 18 | } 19 | 20 | const currentConfig = process.env.LIB_NAME 21 | ? config[process.env.LIB_NAME] 22 | : config.tool; 23 | 24 | export default defineConfig({ 25 | plugins: [vue()], 26 | 27 | define: { 28 | "process.env": process.env, // Vite ditched process.env, so we need to pass it in 29 | }, 30 | 31 | resolve: { 32 | alias: [ 33 | { 34 | find: 'laravel-nova', 35 | replacement: resolve( 36 | __dirname, 37 | 'vendor/laravel/nova/resources/js/mixins/packages.js' 38 | ), 39 | } 40 | ] 41 | }, 42 | 43 | build: { 44 | outDir: resolve(__dirname, "dist"), 45 | emptyOutDir: false, 46 | target: "ES2022", 47 | minify: true, 48 | manifest: true, 49 | lib: { 50 | ...currentConfig, 51 | }, 52 | rollupOptions: { 53 | input: currentConfig.entry, 54 | external: ["vue", "laravel-nova"], 55 | output: { 56 | globals: { 57 | vue: "Vue", 58 | nova: "Nova", 59 | "laravel-nova": "LaravelNova" 60 | }, 61 | assetFileNames: "css/tool.css" 62 | } 63 | }, 64 | }, 65 | 66 | optimizeDeps: { 67 | include: ["vue", "@inertiajs/inertia", "@inertiajs/inertia-vue3", "axios"], 68 | }, 69 | }); 70 | --------------------------------------------------------------------------------