FilesystemManager.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. <?php
  2. namespace Illuminate\Filesystem;
  3. use Aws\S3\S3Client;
  4. use Closure;
  5. use Illuminate\Contracts\Filesystem\Factory as FactoryContract;
  6. use Illuminate\Support\Arr;
  7. use InvalidArgumentException;
  8. use League\Flysystem\AwsS3V3\AwsS3V3Adapter as S3Adapter;
  9. use League\Flysystem\AwsS3V3\PortableVisibilityConverter as AwsS3PortableVisibilityConverter;
  10. use League\Flysystem\Filesystem as Flysystem;
  11. use League\Flysystem\FilesystemAdapter as FlysystemAdapter;
  12. use League\Flysystem\Ftp\FtpAdapter;
  13. use League\Flysystem\Ftp\FtpConnectionOptions;
  14. use League\Flysystem\Local\LocalFilesystemAdapter as LocalAdapter;
  15. use League\Flysystem\PathPrefixing\PathPrefixedAdapter;
  16. use League\Flysystem\PhpseclibV3\SftpAdapter;
  17. use League\Flysystem\PhpseclibV3\SftpConnectionProvider;
  18. use League\Flysystem\ReadOnly\ReadOnlyFilesystemAdapter;
  19. use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
  20. use League\Flysystem\Visibility;
  21. use function Illuminate\Support\enum_value;
  22. /**
  23. * @mixin \Illuminate\Contracts\Filesystem\Filesystem
  24. * @mixin \Illuminate\Filesystem\FilesystemAdapter
  25. */
  26. class FilesystemManager implements FactoryContract
  27. {
  28. /**
  29. * The application instance.
  30. *
  31. * @var \Illuminate\Contracts\Foundation\Application
  32. */
  33. protected $app;
  34. /**
  35. * The array of resolved filesystem drivers.
  36. *
  37. * @var array
  38. */
  39. protected $disks = [];
  40. /**
  41. * The registered custom driver creators.
  42. *
  43. * @var array
  44. */
  45. protected $customCreators = [];
  46. /**
  47. * Create a new filesystem manager instance.
  48. *
  49. * @param \Illuminate\Contracts\Foundation\Application $app
  50. */
  51. public function __construct($app)
  52. {
  53. $this->app = $app;
  54. }
  55. /**
  56. * Get a filesystem instance.
  57. *
  58. * @param string|null $name
  59. * @return \Illuminate\Contracts\Filesystem\Filesystem
  60. */
  61. public function drive($name = null)
  62. {
  63. return $this->disk($name);
  64. }
  65. /**
  66. * Get a filesystem instance.
  67. *
  68. * @param \UnitEnum|string|null $name
  69. * @return \Illuminate\Contracts\Filesystem\Filesystem
  70. */
  71. public function disk($name = null)
  72. {
  73. $name = enum_value($name) ?: $this->getDefaultDriver();
  74. return $this->disks[$name] = $this->get($name);
  75. }
  76. /**
  77. * Get a default cloud filesystem instance.
  78. *
  79. * @return \Illuminate\Contracts\Filesystem\Cloud
  80. */
  81. public function cloud()
  82. {
  83. $name = $this->getDefaultCloudDriver();
  84. return $this->disks[$name] = $this->get($name);
  85. }
  86. /**
  87. * Build an on-demand disk.
  88. *
  89. * @param string|array $config
  90. * @return \Illuminate\Contracts\Filesystem\Filesystem
  91. */
  92. public function build($config)
  93. {
  94. return $this->resolve('ondemand', is_array($config) ? $config : [
  95. 'driver' => 'local',
  96. 'root' => $config,
  97. ]);
  98. }
  99. /**
  100. * Attempt to get the disk from the local cache.
  101. *
  102. * @param string $name
  103. * @return \Illuminate\Contracts\Filesystem\Filesystem
  104. */
  105. protected function get($name)
  106. {
  107. return $this->disks[$name] ?? $this->resolve($name);
  108. }
  109. /**
  110. * Resolve the given disk.
  111. *
  112. * @param string $name
  113. * @param array|null $config
  114. * @return \Illuminate\Contracts\Filesystem\Filesystem
  115. *
  116. * @throws \InvalidArgumentException
  117. */
  118. protected function resolve($name, $config = null)
  119. {
  120. $config ??= $this->getConfig($name);
  121. if (empty($config['driver'])) {
  122. throw new InvalidArgumentException("Disk [{$name}] does not have a configured driver.");
  123. }
  124. $driver = $config['driver'];
  125. if (isset($this->customCreators[$driver])) {
  126. return $this->callCustomCreator($config);
  127. }
  128. $driverMethod = 'create'.ucfirst($driver).'Driver';
  129. if (! method_exists($this, $driverMethod)) {
  130. throw new InvalidArgumentException("Driver [{$driver}] is not supported.");
  131. }
  132. return $this->{$driverMethod}($config, $name);
  133. }
  134. /**
  135. * Call a custom driver creator.
  136. *
  137. * @param array $config
  138. * @return \Illuminate\Contracts\Filesystem\Filesystem
  139. */
  140. protected function callCustomCreator(array $config)
  141. {
  142. return $this->customCreators[$config['driver']]($this->app, $config);
  143. }
  144. /**
  145. * Create an instance of the local driver.
  146. *
  147. * @param array $config
  148. * @param string $name
  149. * @return \Illuminate\Contracts\Filesystem\Filesystem
  150. */
  151. public function createLocalDriver(array $config, string $name = 'local')
  152. {
  153. $visibility = PortableVisibilityConverter::fromArray(
  154. $config['permissions'] ?? [],
  155. $config['directory_visibility'] ?? $config['visibility'] ?? Visibility::PRIVATE
  156. );
  157. $links = ($config['links'] ?? null) === 'skip'
  158. ? LocalAdapter::SKIP_LINKS
  159. : LocalAdapter::DISALLOW_LINKS;
  160. $adapter = new LocalAdapter(
  161. $config['root'], $visibility, $config['lock'] ?? LOCK_EX, $links
  162. );
  163. return (new LocalFilesystemAdapter(
  164. $this->createFlysystem($adapter, $config), $adapter, $config
  165. ))->diskName(
  166. $name
  167. )->shouldServeSignedUrls(
  168. $config['serve'] ?? false,
  169. fn () => $this->app['url'],
  170. );
  171. }
  172. /**
  173. * Create an instance of the ftp driver.
  174. *
  175. * @param array $config
  176. * @return \Illuminate\Contracts\Filesystem\Filesystem
  177. */
  178. public function createFtpDriver(array $config)
  179. {
  180. if (! isset($config['root'])) {
  181. $config['root'] = '';
  182. }
  183. $adapter = new FtpAdapter(FtpConnectionOptions::fromArray($config));
  184. return new FilesystemAdapter($this->createFlysystem($adapter, $config), $adapter, $config);
  185. }
  186. /**
  187. * Create an instance of the sftp driver.
  188. *
  189. * @param array $config
  190. * @return \Illuminate\Contracts\Filesystem\Filesystem
  191. */
  192. public function createSftpDriver(array $config)
  193. {
  194. $provider = SftpConnectionProvider::fromArray($config);
  195. $root = $config['root'] ?? '';
  196. $visibility = PortableVisibilityConverter::fromArray(
  197. $config['permissions'] ?? []
  198. );
  199. $adapter = new SftpAdapter($provider, $root, $visibility);
  200. return new FilesystemAdapter($this->createFlysystem($adapter, $config), $adapter, $config);
  201. }
  202. /**
  203. * Create an instance of the Amazon S3 driver.
  204. *
  205. * @param array $config
  206. * @return \Illuminate\Contracts\Filesystem\Cloud
  207. */
  208. public function createS3Driver(array $config)
  209. {
  210. $s3Config = $this->formatS3Config($config);
  211. $root = (string) ($s3Config['root'] ?? '');
  212. $visibility = new AwsS3PortableVisibilityConverter(
  213. $config['visibility'] ?? Visibility::PUBLIC
  214. );
  215. $streamReads = $s3Config['stream_reads'] ?? false;
  216. $client = new S3Client($s3Config);
  217. $adapter = new S3Adapter($client, $s3Config['bucket'], $root, $visibility, null, $config['options'] ?? [], $streamReads);
  218. return new AwsS3V3Adapter(
  219. $this->createFlysystem($adapter, $config), $adapter, $s3Config, $client
  220. );
  221. }
  222. /**
  223. * Format the given S3 configuration with the default options.
  224. *
  225. * @param array $config
  226. * @return array
  227. */
  228. protected function formatS3Config(array $config)
  229. {
  230. $config += ['version' => 'latest'];
  231. if (! empty($config['key']) && ! empty($config['secret'])) {
  232. $config['credentials'] = Arr::only($config, ['key', 'secret']);
  233. if (! empty($config['token'])) {
  234. $config['credentials']['token'] = $config['token'];
  235. }
  236. }
  237. return Arr::except($config, ['token']);
  238. }
  239. /**
  240. * Create a scoped driver.
  241. *
  242. * @param array $config
  243. * @return \Illuminate\Contracts\Filesystem\Filesystem
  244. *
  245. * @throws \InvalidArgumentException
  246. */
  247. public function createScopedDriver(array $config)
  248. {
  249. if (empty($config['disk'])) {
  250. throw new InvalidArgumentException('Scoped disk is missing "disk" configuration option.');
  251. } elseif (empty($config['prefix'])) {
  252. throw new InvalidArgumentException('Scoped disk is missing "prefix" configuration option.');
  253. }
  254. return $this->build(tap(
  255. is_string($config['disk']) ? $this->getConfig($config['disk']) : $config['disk'],
  256. function (&$parent) use ($config) {
  257. if (empty($parent['prefix'])) {
  258. $parent['prefix'] = $config['prefix'];
  259. } else {
  260. $separator = $parent['directory_separator'] ?? DIRECTORY_SEPARATOR;
  261. $parentPrefix = rtrim($parent['prefix'], $separator);
  262. $scopedPrefix = ltrim($config['prefix'], $separator);
  263. $parent['prefix'] = "{$parentPrefix}{$separator}{$scopedPrefix}";
  264. }
  265. if (isset($config['visibility'])) {
  266. $parent['visibility'] = $config['visibility'];
  267. }
  268. if (isset($config['throw'])) {
  269. $parent['throw'] = $config['throw'];
  270. }
  271. }
  272. ));
  273. }
  274. /**
  275. * Create a Flysystem instance with the given adapter.
  276. *
  277. * @param \League\Flysystem\FilesystemAdapter $adapter
  278. * @param array $config
  279. * @return \League\Flysystem\FilesystemOperator
  280. */
  281. protected function createFlysystem(FlysystemAdapter $adapter, array $config)
  282. {
  283. if ($config['read-only'] ?? false) {
  284. $adapter = new ReadOnlyFilesystemAdapter($adapter);
  285. }
  286. if (! empty($config['prefix'])) {
  287. $adapter = new PathPrefixedAdapter($adapter, $config['prefix']);
  288. }
  289. if (str_contains($config['endpoint'] ?? '', 'r2.cloudflarestorage.com')) {
  290. $config['retain_visibility'] = false;
  291. }
  292. return new Flysystem($adapter, Arr::only($config, [
  293. 'directory_visibility',
  294. 'disable_asserts',
  295. 'retain_visibility',
  296. 'temporary_url',
  297. 'url',
  298. 'visibility',
  299. ]));
  300. }
  301. /**
  302. * Set the given disk instance.
  303. *
  304. * @param string $name
  305. * @param mixed $disk
  306. * @return $this
  307. */
  308. public function set($name, $disk)
  309. {
  310. $this->disks[$name] = $disk;
  311. return $this;
  312. }
  313. /**
  314. * Get the filesystem connection configuration.
  315. *
  316. * @param string $name
  317. * @return array
  318. */
  319. protected function getConfig($name)
  320. {
  321. return $this->app['config']["filesystems.disks.{$name}"] ?: [];
  322. }
  323. /**
  324. * Get the default driver name.
  325. *
  326. * @return string
  327. */
  328. public function getDefaultDriver()
  329. {
  330. return $this->app['config']['filesystems.default'];
  331. }
  332. /**
  333. * Get the default cloud driver name.
  334. *
  335. * @return string
  336. */
  337. public function getDefaultCloudDriver()
  338. {
  339. return $this->app['config']['filesystems.cloud'] ?? 's3';
  340. }
  341. /**
  342. * Unset the given disk instances.
  343. *
  344. * @param array|string $disk
  345. * @return $this
  346. */
  347. public function forgetDisk($disk)
  348. {
  349. foreach ((array) $disk as $diskName) {
  350. unset($this->disks[$diskName]);
  351. }
  352. return $this;
  353. }
  354. /**
  355. * Disconnect the given disk and remove from local cache.
  356. *
  357. * @param string|null $name
  358. * @return void
  359. */
  360. public function purge($name = null)
  361. {
  362. $name ??= $this->getDefaultDriver();
  363. unset($this->disks[$name]);
  364. }
  365. /**
  366. * Register a custom driver creator Closure.
  367. *
  368. * @param string $driver
  369. * @param \Closure $callback
  370. * @return $this
  371. */
  372. public function extend($driver, Closure $callback)
  373. {
  374. $this->customCreators[$driver] = $callback;
  375. return $this;
  376. }
  377. /**
  378. * Set the application instance used by the manager.
  379. *
  380. * @param \Illuminate\Contracts\Foundation\Application $app
  381. * @return $this
  382. */
  383. public function setApplication($app)
  384. {
  385. $this->app = $app;
  386. return $this;
  387. }
  388. /**
  389. * Dynamically call the default driver instance.
  390. *
  391. * @param string $method
  392. * @param array $parameters
  393. * @return mixed
  394. */
  395. public function __call($method, $parameters)
  396. {
  397. return $this->disk()->$method(...$parameters);
  398. }
  399. }