FilesystemAdapter.php 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. <?php
  2. namespace Illuminate\Filesystem;
  3. use RuntimeException;
  4. use Illuminate\Http\File;
  5. use Illuminate\Support\Str;
  6. use InvalidArgumentException;
  7. use Illuminate\Support\Carbon;
  8. use Illuminate\Http\UploadedFile;
  9. use Illuminate\Support\Collection;
  10. use League\Flysystem\AdapterInterface;
  11. use PHPUnit\Framework\Assert as PHPUnit;
  12. use League\Flysystem\FilesystemInterface;
  13. use League\Flysystem\AwsS3v3\AwsS3Adapter;
  14. use League\Flysystem\Cached\CachedAdapter;
  15. use League\Flysystem\FileNotFoundException;
  16. use League\Flysystem\Rackspace\RackspaceAdapter;
  17. use League\Flysystem\Adapter\Local as LocalAdapter;
  18. use Symfony\Component\HttpFoundation\StreamedResponse;
  19. use Illuminate\Contracts\Filesystem\Cloud as CloudFilesystemContract;
  20. use Illuminate\Contracts\Filesystem\Filesystem as FilesystemContract;
  21. use Illuminate\Contracts\Filesystem\FileNotFoundException as ContractFileNotFoundException;
  22. /**
  23. * @mixin \League\Flysystem\FilesystemInterface
  24. */
  25. class FilesystemAdapter implements FilesystemContract, CloudFilesystemContract
  26. {
  27. /**
  28. * The Flysystem filesystem implementation.
  29. *
  30. * @var \League\Flysystem\FilesystemInterface
  31. */
  32. protected $driver;
  33. /**
  34. * Create a new filesystem adapter instance.
  35. *
  36. * @param \League\Flysystem\FilesystemInterface $driver
  37. * @return void
  38. */
  39. public function __construct(FilesystemInterface $driver)
  40. {
  41. $this->driver = $driver;
  42. }
  43. /**
  44. * Assert that the given file exists.
  45. *
  46. * @param string $path
  47. * @return void
  48. */
  49. public function assertExists($path)
  50. {
  51. PHPUnit::assertTrue(
  52. $this->exists($path), "Unable to find a file at path [{$path}]."
  53. );
  54. }
  55. /**
  56. * Assert that the given file does not exist.
  57. *
  58. * @param string $path
  59. * @return void
  60. */
  61. public function assertMissing($path)
  62. {
  63. PHPUnit::assertFalse(
  64. $this->exists($path), "Found unexpected file at path [{$path}]."
  65. );
  66. }
  67. /**
  68. * Determine if a file exists.
  69. *
  70. * @param string $path
  71. * @return bool
  72. */
  73. public function exists($path)
  74. {
  75. return $this->driver->has($path);
  76. }
  77. /**
  78. * Get the full path for the file at the given "short" path.
  79. *
  80. * @param string $path
  81. * @return string
  82. */
  83. public function path($path)
  84. {
  85. return $this->driver->getAdapter()->getPathPrefix().$path;
  86. }
  87. /**
  88. * Get the contents of a file.
  89. *
  90. * @param string $path
  91. * @return string
  92. *
  93. * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
  94. */
  95. public function get($path)
  96. {
  97. try {
  98. return $this->driver->read($path);
  99. } catch (FileNotFoundException $e) {
  100. throw new ContractFileNotFoundException($path, $e->getCode(), $e);
  101. }
  102. }
  103. /**
  104. * Create a streamed response for a given file.
  105. *
  106. * @param string $path
  107. * @param string|null $name
  108. * @param array|null $headers
  109. * @param string|null $disposition
  110. * @return \Symfony\Component\HttpFoundation\StreamedResponse
  111. */
  112. public function response($path, $name = null, array $headers = [], $disposition = 'inline')
  113. {
  114. $response = new StreamedResponse;
  115. $disposition = $response->headers->makeDisposition($disposition, $name ?? basename($path));
  116. $response->headers->replace($headers + [
  117. 'Content-Type' => $this->mimeType($path),
  118. 'Content-Length' => $this->size($path),
  119. 'Content-Disposition' => $disposition,
  120. ]);
  121. $response->setCallback(function () use ($path) {
  122. $stream = $this->driver->readStream($path);
  123. fpassthru($stream);
  124. fclose($stream);
  125. });
  126. return $response;
  127. }
  128. /**
  129. * Create a streamed download response for a given file.
  130. *
  131. * @param string $path
  132. * @param string|null $name
  133. * @param array|null $headers
  134. * @return \Symfony\Component\HttpFoundation\StreamedResponse
  135. */
  136. public function download($path, $name = null, array $headers = [])
  137. {
  138. return $this->response($path, $name, $headers, 'attachment');
  139. }
  140. /**
  141. * Write the contents of a file.
  142. *
  143. * @param string $path
  144. * @param string|resource $contents
  145. * @param mixed $options
  146. * @return bool
  147. */
  148. public function put($path, $contents, $options = [])
  149. {
  150. $options = is_string($options)
  151. ? ['visibility' => $options]
  152. : (array) $options;
  153. // If the given contents is actually a file or uploaded file instance than we will
  154. // automatically store the file using a stream. This provides a convenient path
  155. // for the developer to store streams without managing them manually in code.
  156. if ($contents instanceof File ||
  157. $contents instanceof UploadedFile) {
  158. return $this->putFile($path, $contents, $options);
  159. }
  160. return is_resource($contents)
  161. ? $this->driver->putStream($path, $contents, $options)
  162. : $this->driver->put($path, $contents, $options);
  163. }
  164. /**
  165. * Store the uploaded file on the disk.
  166. *
  167. * @param string $path
  168. * @param \Illuminate\Http\File|\Illuminate\Http\UploadedFile $file
  169. * @param array $options
  170. * @return string|false
  171. */
  172. public function putFile($path, $file, $options = [])
  173. {
  174. return $this->putFileAs($path, $file, $file->hashName(), $options);
  175. }
  176. /**
  177. * Store the uploaded file on the disk with a given name.
  178. *
  179. * @param string $path
  180. * @param \Illuminate\Http\File|\Illuminate\Http\UploadedFile $file
  181. * @param string $name
  182. * @param array $options
  183. * @return string|false
  184. */
  185. public function putFileAs($path, $file, $name, $options = [])
  186. {
  187. $stream = fopen($file->getRealPath(), 'r+');
  188. // Next, we will format the path of the file and store the file using a stream since
  189. // they provide better performance than alternatives. Once we write the file this
  190. // stream will get closed automatically by us so the developer doesn't have to.
  191. $result = $this->put(
  192. $path = trim($path.'/'.$name, '/'), $stream, $options
  193. );
  194. if (is_resource($stream)) {
  195. fclose($stream);
  196. }
  197. return $result ? $path : false;
  198. }
  199. /**
  200. * Get the visibility for the given path.
  201. *
  202. * @param string $path
  203. * @return string
  204. */
  205. public function getVisibility($path)
  206. {
  207. if ($this->driver->getVisibility($path) == AdapterInterface::VISIBILITY_PUBLIC) {
  208. return FilesystemContract::VISIBILITY_PUBLIC;
  209. }
  210. return FilesystemContract::VISIBILITY_PRIVATE;
  211. }
  212. /**
  213. * Set the visibility for the given path.
  214. *
  215. * @param string $path
  216. * @param string $visibility
  217. * @return void
  218. */
  219. public function setVisibility($path, $visibility)
  220. {
  221. return $this->driver->setVisibility($path, $this->parseVisibility($visibility));
  222. }
  223. /**
  224. * Prepend to a file.
  225. *
  226. * @param string $path
  227. * @param string $data
  228. * @param string $separator
  229. * @return int
  230. */
  231. public function prepend($path, $data, $separator = PHP_EOL)
  232. {
  233. if ($this->exists($path)) {
  234. return $this->put($path, $data.$separator.$this->get($path));
  235. }
  236. return $this->put($path, $data);
  237. }
  238. /**
  239. * Append to a file.
  240. *
  241. * @param string $path
  242. * @param string $data
  243. * @param string $separator
  244. * @return int
  245. */
  246. public function append($path, $data, $separator = PHP_EOL)
  247. {
  248. if ($this->exists($path)) {
  249. return $this->put($path, $this->get($path).$separator.$data);
  250. }
  251. return $this->put($path, $data);
  252. }
  253. /**
  254. * Delete the file at a given path.
  255. *
  256. * @param string|array $paths
  257. * @return bool
  258. */
  259. public function delete($paths)
  260. {
  261. $paths = is_array($paths) ? $paths : func_get_args();
  262. $success = true;
  263. foreach ($paths as $path) {
  264. try {
  265. if (! $this->driver->delete($path)) {
  266. $success = false;
  267. }
  268. } catch (FileNotFoundException $e) {
  269. $success = false;
  270. }
  271. }
  272. return $success;
  273. }
  274. /**
  275. * Copy a file to a new location.
  276. *
  277. * @param string $from
  278. * @param string $to
  279. * @return bool
  280. */
  281. public function copy($from, $to)
  282. {
  283. return $this->driver->copy($from, $to);
  284. }
  285. /**
  286. * Move a file to a new location.
  287. *
  288. * @param string $from
  289. * @param string $to
  290. * @return bool
  291. */
  292. public function move($from, $to)
  293. {
  294. return $this->driver->rename($from, $to);
  295. }
  296. /**
  297. * Get the file size of a given file.
  298. *
  299. * @param string $path
  300. * @return int
  301. */
  302. public function size($path)
  303. {
  304. return $this->driver->getSize($path);
  305. }
  306. /**
  307. * Get the mime-type of a given file.
  308. *
  309. * @param string $path
  310. * @return string|false
  311. */
  312. public function mimeType($path)
  313. {
  314. return $this->driver->getMimetype($path);
  315. }
  316. /**
  317. * Get the file's last modification time.
  318. *
  319. * @param string $path
  320. * @return int
  321. */
  322. public function lastModified($path)
  323. {
  324. return $this->driver->getTimestamp($path);
  325. }
  326. /**
  327. * Get the URL for the file at the given path.
  328. *
  329. * @param string $path
  330. * @return string
  331. */
  332. public function url($path)
  333. {
  334. $adapter = $this->driver->getAdapter();
  335. if ($adapter instanceof CachedAdapter) {
  336. $adapter = $adapter->getAdapter();
  337. }
  338. if (method_exists($adapter, 'getUrl')) {
  339. return $adapter->getUrl($path);
  340. } elseif (method_exists($this->driver, 'getUrl')) {
  341. return $this->driver->getUrl($path);
  342. } elseif ($adapter instanceof AwsS3Adapter) {
  343. return $this->getAwsUrl($adapter, $path);
  344. } elseif ($adapter instanceof RackspaceAdapter) {
  345. return $this->getRackspaceUrl($adapter, $path);
  346. } elseif ($adapter instanceof LocalAdapter) {
  347. return $this->getLocalUrl($path);
  348. } else {
  349. throw new RuntimeException('This driver does not support retrieving URLs.');
  350. }
  351. }
  352. /**
  353. * Get the URL for the file at the given path.
  354. *
  355. * @param \League\Flysystem\AwsS3v3\AwsS3Adapter $adapter
  356. * @param string $path
  357. * @return string
  358. */
  359. protected function getAwsUrl($adapter, $path)
  360. {
  361. // If an explicit base URL has been set on the disk configuration then we will use
  362. // it as the base URL instead of the default path. This allows the developer to
  363. // have full control over the base path for this filesystem's generated URLs.
  364. if (! is_null($url = $this->driver->getConfig()->get('url'))) {
  365. return $this->concatPathToUrl($url, $adapter->getPathPrefix().$path);
  366. }
  367. return $adapter->getClient()->getObjectUrl(
  368. $adapter->getBucket(), $adapter->getPathPrefix().$path
  369. );
  370. }
  371. /**
  372. * Get the URL for the file at the given path.
  373. *
  374. * @param \League\Flysystem\Rackspace\RackspaceAdapter $adapter
  375. * @param string $path
  376. * @return string
  377. */
  378. protected function getRackspaceUrl($adapter, $path)
  379. {
  380. return (string) $adapter->getContainer()->getObject($path)->getPublicUrl();
  381. }
  382. /**
  383. * Get the URL for the file at the given path.
  384. *
  385. * @param string $path
  386. * @return string
  387. */
  388. protected function getLocalUrl($path)
  389. {
  390. $config = $this->driver->getConfig();
  391. // If an explicit base URL has been set on the disk configuration then we will use
  392. // it as the base URL instead of the default path. This allows the developer to
  393. // have full control over the base path for this filesystem's generated URLs.
  394. if ($config->has('url')) {
  395. return $this->concatPathToUrl($config->get('url'), $path);
  396. }
  397. $path = '/storage/'.$path;
  398. // If the path contains "storage/public", it probably means the developer is using
  399. // the default disk to generate the path instead of the "public" disk like they
  400. // are really supposed to use. We will remove the public from this path here.
  401. if (Str::contains($path, '/storage/public/')) {
  402. return Str::replaceFirst('/public/', '/', $path);
  403. }
  404. return $path;
  405. }
  406. /**
  407. * Get a temporary URL for the file at the given path.
  408. *
  409. * @param string $path
  410. * @param \DateTimeInterface $expiration
  411. * @param array $options
  412. * @return string
  413. */
  414. public function temporaryUrl($path, $expiration, array $options = [])
  415. {
  416. $adapter = $this->driver->getAdapter();
  417. if ($adapter instanceof CachedAdapter) {
  418. $adapter = $adapter->getAdapter();
  419. }
  420. if (method_exists($adapter, 'getTemporaryUrl')) {
  421. return $adapter->getTemporaryUrl($path, $expiration, $options);
  422. } elseif ($adapter instanceof AwsS3Adapter) {
  423. return $this->getAwsTemporaryUrl($adapter, $path, $expiration, $options);
  424. } elseif ($adapter instanceof RackspaceAdapter) {
  425. return $this->getRackspaceTemporaryUrl($adapter, $path, $expiration, $options);
  426. } else {
  427. throw new RuntimeException('This driver does not support creating temporary URLs.');
  428. }
  429. }
  430. /**
  431. * Get a temporary URL for the file at the given path.
  432. *
  433. * @param \League\Flysystem\AwsS3v3\AwsS3Adapter $adapter
  434. * @param string $path
  435. * @param \DateTimeInterface $expiration
  436. * @param array $options
  437. * @return string
  438. */
  439. public function getAwsTemporaryUrl($adapter, $path, $expiration, $options)
  440. {
  441. $client = $adapter->getClient();
  442. $command = $client->getCommand('GetObject', array_merge([
  443. 'Bucket' => $adapter->getBucket(),
  444. 'Key' => $adapter->getPathPrefix().$path,
  445. ], $options));
  446. return (string) $client->createPresignedRequest(
  447. $command, $expiration
  448. )->getUri();
  449. }
  450. /**
  451. * Get a temporary URL for the file at the given path.
  452. *
  453. * @param \League\Flysystem\Rackspace\RackspaceAdapter $adapter
  454. * @param string $path
  455. * @param \DateTimeInterface $expiration
  456. * @param array $options
  457. * @return string
  458. */
  459. public function getRackspaceTemporaryUrl($adapter, $path, $expiration, $options)
  460. {
  461. return $adapter->getContainer()->getObject($path)->getTemporaryUrl(
  462. Carbon::now()->diffInSeconds($expiration),
  463. $options['method'] ?? 'GET',
  464. $options['forcePublicUrl'] ?? true
  465. );
  466. }
  467. /**
  468. * Concatenate a path to a URL.
  469. *
  470. * @param string $url
  471. * @param string $path
  472. * @return string
  473. */
  474. protected function concatPathToUrl($url, $path)
  475. {
  476. return rtrim($url, '/').'/'.ltrim($path, '/');
  477. }
  478. /**
  479. * Get an array of all files in a directory.
  480. *
  481. * @param string|null $directory
  482. * @param bool $recursive
  483. * @return array
  484. */
  485. public function files($directory = null, $recursive = false)
  486. {
  487. $contents = $this->driver->listContents($directory, $recursive);
  488. return $this->filterContentsByType($contents, 'file');
  489. }
  490. /**
  491. * Get all of the files from the given directory (recursive).
  492. *
  493. * @param string|null $directory
  494. * @return array
  495. */
  496. public function allFiles($directory = null)
  497. {
  498. return $this->files($directory, true);
  499. }
  500. /**
  501. * Get all of the directories within a given directory.
  502. *
  503. * @param string|null $directory
  504. * @param bool $recursive
  505. * @return array
  506. */
  507. public function directories($directory = null, $recursive = false)
  508. {
  509. $contents = $this->driver->listContents($directory, $recursive);
  510. return $this->filterContentsByType($contents, 'dir');
  511. }
  512. /**
  513. * Get all (recursive) of the directories within a given directory.
  514. *
  515. * @param string|null $directory
  516. * @return array
  517. */
  518. public function allDirectories($directory = null)
  519. {
  520. return $this->directories($directory, true);
  521. }
  522. /**
  523. * Create a directory.
  524. *
  525. * @param string $path
  526. * @return bool
  527. */
  528. public function makeDirectory($path)
  529. {
  530. return $this->driver->createDir($path);
  531. }
  532. /**
  533. * Recursively delete a directory.
  534. *
  535. * @param string $directory
  536. * @return bool
  537. */
  538. public function deleteDirectory($directory)
  539. {
  540. return $this->driver->deleteDir($directory);
  541. }
  542. /**
  543. * Flush the Flysystem cache.
  544. *
  545. * @return void
  546. */
  547. public function flushCache()
  548. {
  549. $adapter = $this->driver->getAdapter();
  550. if ($adapter instanceof CachedAdapter) {
  551. $adapter->getCache()->flush();
  552. }
  553. }
  554. /**
  555. * Get the Flysystem driver.
  556. *
  557. * @return \League\Flysystem\FilesystemInterface
  558. */
  559. public function getDriver()
  560. {
  561. return $this->driver;
  562. }
  563. /**
  564. * Filter directory contents by type.
  565. *
  566. * @param array $contents
  567. * @param string $type
  568. * @return array
  569. */
  570. protected function filterContentsByType($contents, $type)
  571. {
  572. return Collection::make($contents)
  573. ->where('type', $type)
  574. ->pluck('path')
  575. ->values()
  576. ->all();
  577. }
  578. /**
  579. * Parse the given visibility value.
  580. *
  581. * @param string|null $visibility
  582. * @return string|null
  583. *
  584. * @throws \InvalidArgumentException
  585. */
  586. protected function parseVisibility($visibility)
  587. {
  588. if (is_null($visibility)) {
  589. return;
  590. }
  591. switch ($visibility) {
  592. case FilesystemContract::VISIBILITY_PUBLIC:
  593. return AdapterInterface::VISIBILITY_PUBLIC;
  594. case FilesystemContract::VISIBILITY_PRIVATE:
  595. return AdapterInterface::VISIBILITY_PRIVATE;
  596. }
  597. throw new InvalidArgumentException("Unknown visibility: {$visibility}");
  598. }
  599. /**
  600. * Pass dynamic methods call onto Flysystem.
  601. *
  602. * @param string $method
  603. * @param array $parameters
  604. * @return mixed
  605. *
  606. * @throws \BadMethodCallException
  607. */
  608. public function __call($method, array $parameters)
  609. {
  610. return call_user_func_array([$this->driver, $method], $parameters);
  611. }
  612. }