├── .gitignore ├── LICENSE ├── README.md ├── composer.json └── src └── VueFinder.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | storage 3 | test 4 | public 5 | composer.lock 6 | .DS_Store 7 | .idea 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yusuf Özdemir 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuefinder-php 2 | php serverside library for vuefinder 3 | 4 | ![ezgif-1-b902690b76](https://user-images.githubusercontent.com/712404/193141338-8d5f726f-da1a-4825-b652-28e4007493db.gif) 5 | 6 | 7 | frontend (vue3) : https://github.com/n1crack/vuefinder 8 | 9 | ## Installation 10 | ``` 11 | composer require ozdemir/vuefinder-php 12 | ``` 13 | ## Usage 14 | ```php 15 | require '../vendor/autoload.php'; 16 | 17 | use Ozdemir\VueFinder\Vuefinder; 18 | use League\Flysystem\Local\LocalFilesystemAdapter; 19 | 20 | // Set VueFinder class 21 | $vuefinder = new VueFinder([ 22 | 'local' => new LocalFilesystemAdapter(dirname(__DIR__).'/storage'), 23 | 'test' => new LocalFilesystemAdapter(dirname(__DIR__).'/test'), 24 | ]); 25 | 26 | 27 | $config = [ 28 | 'publicLinks' => [ 29 | 'local://public' => 'http://example.test', 30 | ], 31 | ]; 32 | 33 | // Perform the class 34 | $vuefinder->init($config); 35 | ``` 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ozdemir/vuefinder-php", 3 | "description": "Vuefinder Php Library.", 4 | "license": "MIT", 5 | "config": { 6 | "bin-dir": "vendor/bin" 7 | }, 8 | "authors": [ 9 | { 10 | "name": "Yusuf Özdemir", 11 | "email": "yusuf@ozdemir.be" 12 | } 13 | ], 14 | "autoload": { 15 | "psr-4": { 16 | "Ozdemir\\VueFinder\\": "src/" 17 | } 18 | }, 19 | "require": { 20 | "symfony/http-foundation": "^7.0", 21 | "league/flysystem": "^3.0", 22 | "league/flysystem-ziparchive": "^3.2", 23 | "league/flysystem-read-only": "^3.3" 24 | }, 25 | "minimum-stability": "stable" 26 | } 27 | -------------------------------------------------------------------------------- /src/VueFinder.php: -------------------------------------------------------------------------------- 1 | storageAdapters = $storages; 39 | 40 | $this->request = Request::createFromGlobals(); 41 | 42 | $this->adapterKey = $this->request->get('adapter'); 43 | 44 | if (!$this->adapterKey || !in_array($this->adapterKey, array_keys($storages)) ) { 45 | $this->adapterKey = array_keys($storages)[0]; 46 | } 47 | 48 | $this->storages = array_keys($storages); 49 | 50 | $storages = array_map(static fn($adapter) => new Filesystem($adapter), $storages); 51 | 52 | $this->manager = new MountManager($storages); 53 | } 54 | 55 | /** 56 | * @param $files 57 | * @return array 58 | */ 59 | public function directories($files): array 60 | { 61 | return array_filter($files, static fn($item) => $item['type'] == 'dir'); 62 | } 63 | 64 | /** 65 | * @param $files 66 | * @return array 67 | */ 68 | public function files($files, $search = false): array 69 | { 70 | return array_filter( 71 | $files, 72 | static fn($item) => $item['type'] == 'file' && (!$search || fnmatch("*$search*", $item['path'], FNM_CASEFOLD)) 73 | ); 74 | } 75 | 76 | /** 77 | * @param $config 78 | */ 79 | public function init($config): void 80 | { 81 | $this->config = $config; 82 | $query = $this->request->get('q'); 83 | 84 | $route_array = [ 85 | 'index' => 'get', 86 | 'subfolders' => 'get', 87 | 'download' => 'get', 88 | 'preview' => 'get', 89 | 'search' => 'get', 90 | 'newfolder' => 'post', 91 | 'newfile' => 'post', 92 | 'rename' => 'post', 93 | 'move' => 'post', 94 | 'delete' => 'post', 95 | 'upload' => 'post', 96 | 'archive' => 'post', 97 | 'unarchive' => 'post', 98 | 'save' => 'post', 99 | ]; 100 | 101 | if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { 102 | $response = new JsonResponse(); 103 | $response->headers->set('Access-Control-Allow-Origin', "*"); 104 | $response->headers->set('Access-Control-Allow-Headers', "*"); 105 | $response->send(); 106 | return; 107 | } 108 | 109 | try { 110 | if (!array_key_exists($query, $route_array) 111 | || $route_array[$query] !== strtolower($this->request->getMethod())) { 112 | throw new Exception('The query does not have a valid method.'); 113 | } 114 | 115 | $adapter = $this->storageAdapters[$this->adapterKey]; 116 | $readonly_array = ['index', 'download', 'preview', 'search', 'subfolders']; 117 | 118 | if ($adapter instanceof ReadOnlyFilesystemAdapter && !in_array($query, $readonly_array, true)) { 119 | throw new Exception('This is a readonly storage.'); 120 | } 121 | 122 | $response = $this->$query(); 123 | } catch (Exception $e) { 124 | $response = new JsonResponse(['status' => false, 'message' => $e->getMessage()], 400); 125 | } 126 | 127 | $response->headers->set('Access-Control-Allow-Origin', "*"); 128 | $response->headers->set('Access-Control-Allow-Headers', "*"); 129 | 130 | $response->send(); 131 | } 132 | 133 | public function config($key) 134 | { 135 | return $this->config[$key] ?? null; 136 | } 137 | 138 | /** 139 | * @return JsonResponse 140 | * @throws FilesystemException 141 | */ 142 | public function index() 143 | { 144 | $dirname = $this->request->get('path', $this->adapterKey.'://'); 145 | 146 | $listContents = $this->manager 147 | ->listContents($dirname) 148 | ->map(fn(StorageAttributes $attributes) => $attributes->jsonSerialize()) 149 | ->toArray(); 150 | 151 | $files = array_merge( 152 | $this->directories($listContents), 153 | $this->files($listContents) 154 | ); 155 | 156 | $files = array_map(function($node) { 157 | $node['basename'] = basename($node['path']); 158 | $node['extension'] = pathinfo($node['path'], PATHINFO_EXTENSION); 159 | $node['storage'] = $this->adapterKey; 160 | 161 | if ($node['type'] != 'dir' && $node['extension']) { 162 | try { 163 | $node['mime_type'] = $this->manager->mimeType($node['path']); 164 | } catch (Exception $exception) { 165 | // 166 | } 167 | } 168 | $this->setPublicLinks($node); 169 | 170 | return $node; 171 | }, $files); 172 | 173 | $storages = $this->storages; 174 | $adapter = $this->adapterKey; 175 | 176 | return new JsonResponse(compact(['adapter', 'storages', 'dirname', 'files'])); 177 | } 178 | 179 | public function subfolders() 180 | { 181 | $dirname = $this->request->get('path', $this->adapterKey . '://'); 182 | 183 | $folders = $this->manager 184 | ->listContents($dirname) 185 | ->filter(fn(StorageAttributes $attributes) => $attributes->isDir()) 186 | ->map(fn(StorageAttributes $attributes) => [ 187 | 'adapter' => $this->adapterKey, 188 | 'path' => $attributes->path(), 189 | 'basename' => basename($attributes->path()), 190 | ]) 191 | ->toArray();; 192 | 193 | return new JsonResponse(compact(['folders'])); 194 | } 195 | 196 | /** 197 | * @return JsonResponse 198 | * @throws FilesystemException 199 | */ 200 | public function search() 201 | { 202 | $dirname = $this->request->get('path', $this->adapterKey.'://'); 203 | $filter = $this->request->get('filter'); 204 | 205 | $listContents = $this->manager 206 | ->listContents($dirname, true) 207 | ->map(fn(StorageAttributes $attributes) => $attributes->jsonSerialize()) 208 | ->toArray(); 209 | 210 | $files = array_values($this->files($listContents, $filter)); 211 | 212 | $files = array_map(function($node) { 213 | $node['basename'] = basename($node['path']); 214 | $node['extension'] = pathinfo($node['path'], PATHINFO_EXTENSION); 215 | $node['storage'] = $this->adapterKey; 216 | $node['dir'] = dirname($node['path']); 217 | 218 | if ($node['type'] != 'dir') { 219 | try { 220 | $node['mime_type'] = $this->manager->mimeType($node['path']); 221 | } catch (Exception $exception) { 222 | // 223 | } 224 | } 225 | $this->setPublicLinks($node); 226 | 227 | return $node; 228 | }, $files); 229 | 230 | $storages = $this->storages; 231 | $adapter = $this->adapterKey; 232 | 233 | return new JsonResponse(compact(['adapter', 'storages', 'dirname', 'files'])); 234 | } 235 | 236 | /** 237 | * @return JsonResponse 238 | * @throws FilesystemException 239 | */ 240 | public function newfolder() 241 | { 242 | $path = $this->request->get('path'); 243 | $name = $this->request->getPayload()->get('name'); 244 | 245 | if (!$name || !strpbrk($name, "\\/?%*:|\"<>") === false) { 246 | throw new Exception('Invalid folder name.'); 247 | } 248 | 249 | $newPath = $path.DIRECTORY_SEPARATOR.$name; 250 | 251 | if ($this->manager->fileExists($newPath) || $this->manager->directoryExists($newPath)) { 252 | throw new Exception('The file/folder is already exists. Try another name.'); 253 | } 254 | 255 | $this->manager->createDirectory($newPath); 256 | 257 | return $this->index(); 258 | } 259 | 260 | /** 261 | * @return JsonResponse 262 | */ 263 | public function newfile() 264 | { 265 | $path = $this->request->get('path'); 266 | $name = $this->request->getPayload()->get('name'); 267 | 268 | if (!$name || !strpbrk($name, "\\/?%*:|\"<>") === false) { 269 | throw new Exception('Invalid file name.'); 270 | } 271 | $newPath = $path.DIRECTORY_SEPARATOR.$name; 272 | 273 | if ($this->manager->fileExists($newPath) || $this->manager->directoryExists($newPath)) { 274 | throw new Exception('The file/folder is already exists. Try another name.'); 275 | } 276 | 277 | $this->manager->write($newPath, ''); 278 | 279 | return $this->index(); 280 | } 281 | 282 | /** 283 | * @return JsonResponse 284 | * 285 | * @throws FilesystemException 286 | */ 287 | public function upload() 288 | { 289 | $path = $this->request->get('path'); 290 | $name = $this->request->getPayload()->get('name'); 291 | 292 | $file = $this->request->files->get('file'); 293 | $stream = fopen($file->getRealPath(), 'r+'); 294 | 295 | $this->manager->writeStream( 296 | $path.DIRECTORY_SEPARATOR.$name, 297 | $stream 298 | ); 299 | fclose($stream); 300 | 301 | return new JsonResponse(['ok']); 302 | } 303 | 304 | public function preview() 305 | { 306 | $path = $this->request->get('path'); 307 | 308 | return $this->streamFile($path); 309 | } 310 | 311 | 312 | public function save() 313 | { 314 | $path = $this->request->get('path'); 315 | $content = $this->request->getPayload()->get('content'); 316 | 317 | $this->manager->write($path, $content); 318 | 319 | return $this->preview(); 320 | } 321 | 322 | /** 323 | * @return StreamedResponse 324 | * @throws FileNotFoundException 325 | */ 326 | public function download() 327 | { 328 | $path = $this->request->get('path'); 329 | $response = $this->streamFile($path); 330 | 331 | $filenameFallback = preg_replace( 332 | '#^.*\.#', 333 | md5($path) . '.', $path 334 | ); 335 | 336 | $disposition = $response->headers->makeDisposition( 337 | ResponseHeaderBag::DISPOSITION_ATTACHMENT, 338 | basename($path), 339 | $filenameFallback 340 | ); 341 | $response->headers->set('Content-Disposition', $disposition); 342 | 343 | return $response; 344 | } 345 | 346 | /** 347 | * @return JsonResponse 348 | * @throws FilesystemException 349 | */ 350 | public function rename() 351 | { 352 | $payload = $this->request->getPayload(); 353 | $name = $payload->get('name'); 354 | $from = $payload->get('item'); 355 | $path = $this->request->get('path'); 356 | $to = $path.DIRECTORY_SEPARATOR.$name; 357 | 358 | if ($this->manager->fileExists($to) || $this->manager->directoryExists($to)) { 359 | throw new Exception('The file/folder is already exists.'); 360 | } 361 | 362 | $this->manager->move($from, $to); 363 | 364 | return $this->index(); 365 | } 366 | 367 | public function move() 368 | { 369 | $payload = $this->request->getPayload(); 370 | $to = $payload->get('item'); 371 | $items = $payload->all('items'); 372 | 373 | foreach ($items as $item) { 374 | $target = $to.DIRECTORY_SEPARATOR.basename($item['path']); 375 | if ($this->manager->fileExists($target) || $this->manager->directoryExists($target)) { 376 | throw new Exception('One of the files is already exists.'); 377 | } 378 | } 379 | 380 | foreach ($items as $item) { 381 | $target = $to.DIRECTORY_SEPARATOR.basename($item['path']); 382 | 383 | $this->manager->move($item['path'], $target); 384 | } 385 | 386 | return $this->index(); 387 | } 388 | 389 | /** 390 | * @return JsonResponse 391 | * @throws FileNotFoundException 392 | */ 393 | public function delete() 394 | { 395 | $items = $this->request->getPayload()->all("items"); 396 | 397 | foreach ($items as $item) { 398 | if ($item['type'] == 'dir') { 399 | $this->manager->deleteDirectory($item['path']); 400 | } else { 401 | $this->manager->delete($item['path']); 402 | } 403 | } 404 | 405 | return $this->index(); 406 | } 407 | 408 | /** 409 | * @return JsonResponse 410 | * @throws FileNotFoundException 411 | * @throws JsonException|FilesystemException 412 | */ 413 | public function archive() 414 | { 415 | $payload = $this->request->getPayload(); 416 | $name = pathinfo($payload->get('name'), PATHINFO_FILENAME); 417 | if (!$name || !strpbrk($name, "\\/?%*:|\"<>") === false) { 418 | throw new Exception('Invalid file name.'); 419 | } 420 | 421 | $items = $payload->all('items'); 422 | $name .= '.zip'; 423 | $path = $this->request->get('path').DIRECTORY_SEPARATOR.$name; 424 | $zipFile = tempnam(sys_get_temp_dir(), $name); 425 | 426 | if ($this->manager->fileExists($path)) { 427 | throw new Exception('The archive is exists. Try another name.'); 428 | } 429 | 430 | $zipStorage = new Filesystem( 431 | new ZipArchiveAdapter( 432 | new FilesystemZipArchiveProvider( 433 | $zipFile, 434 | ), 435 | ), 436 | ); 437 | 438 | foreach ($items as $item) { 439 | if ($item['type'] == 'dir') { 440 | $dirFiles = $this->manager->listContents($item['path'], true) 441 | ->filter(fn(StorageAttributes $attributes) => $attributes->isFile()) 442 | ->toArray(); 443 | foreach ($dirFiles as $dirFile) { 444 | $file = $this->manager->readStream($dirFile->path()); 445 | $zipStorage->writeStream(str_replace($this->request->get('path'), '', $dirFile->path()), $file); 446 | } 447 | } else { 448 | $file = $this->manager->readStream($item['path']); 449 | $zipStorage->writeStream(str_replace($this->request->get('path'), '', $item['path']), $file); 450 | } 451 | } 452 | 453 | if ($zipStream = fopen($zipFile, 'r')) { 454 | $this->manager->writeStream($path, $zipStream); 455 | fclose($zipStream); 456 | } 457 | unlink($zipFile); 458 | 459 | return $this->index(); 460 | } 461 | 462 | /** 463 | * @return JsonResponse 464 | * @throws FileNotFoundException 465 | * @throws FilesystemException 466 | */ 467 | public function unarchive() 468 | { 469 | $zipItem = $this->request->getPayload()->get('item'); 470 | 471 | $zipStream = $this->manager->readStream($zipItem); 472 | 473 | $zipFile = tempnam(sys_get_temp_dir(), $zipItem); 474 | 475 | file_put_contents($zipFile, $zipStream); 476 | 477 | $zipStorage = new Filesystem( 478 | new ZipArchiveAdapter( 479 | new FilesystemZipArchiveProvider( 480 | $zipFile, 481 | ), 482 | ), 483 | ); 484 | 485 | $dirFiles = $zipStorage->listContents('', true) 486 | ->filter(fn(StorageAttributes $attributes) => $attributes->isFile()) 487 | ->toArray(); 488 | 489 | $path = $this->request->get('path').DIRECTORY_SEPARATOR.pathinfo($zipItem, PATHINFO_FILENAME).DIRECTORY_SEPARATOR; 490 | 491 | 492 | foreach ($dirFiles as $dirFile) { 493 | $file = $zipStorage->readStream($dirFile->path()); 494 | $this->manager->writeStream($path.$dirFile->path(), $file); 495 | } 496 | 497 | unlink($zipFile); 498 | 499 | return $this->index(); 500 | } 501 | 502 | /** 503 | * @param $path 504 | * @return StreamedResponse 505 | * @throws FileNotFoundException|FilesystemException 506 | */ 507 | public function streamFile($path) 508 | { 509 | $stream = $this->manager->readStream($path); 510 | 511 | $response = new StreamedResponse(); 512 | 513 | try { 514 | $mimeType = $this->manager->mimeType($path); 515 | } catch (Exception $exception) { 516 | $mimeType = 'application/octet-stream'; 517 | } 518 | 519 | $size = $this->manager->fileSize($path); 520 | 521 | $response->headers->set('Access-Control-Allow-Origin', "*"); 522 | $response->headers->set('Access-Control-Allow-Headers', "*"); 523 | $response->headers->set('Content-Length', $size); 524 | $response->headers->set('Content-Type', $mimeType); 525 | $response->headers->set('Content-Transfer-Encoding', 'binary'); 526 | $response->headers->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0'); 527 | $response->headers->set('Accept-Ranges', 'bytes'); 528 | 529 | if (isset($_SERVER['HTTP_RANGE'])) { 530 | header('HTTP/1.1 206 Partial Content'); 531 | } 532 | 533 | $response->setCallback(function() use ($stream) { 534 | fpassthru($stream); 535 | }); 536 | 537 | return $response; 538 | } 539 | 540 | private function setPublicLinks(mixed &$node) 541 | { 542 | $publicLinks = $this->config('publicLinks'); 543 | 544 | if ($publicLinks && $node['type'] != 'dir') { 545 | foreach ($publicLinks as $publicLink => $domain) { 546 | $publicLink = str_replace('/', '\/', $publicLink); 547 | 548 | if (preg_match('/^'.$publicLink.'/i', $node['path'])) { 549 | $node['url'] = preg_replace('/^'.$publicLink.'/i', $domain, $node['path']); 550 | } 551 | } 552 | } 553 | } 554 | } 555 | --------------------------------------------------------------------------------