vendor/league/flysystem-aws-s3-v3/AwsS3V3Adapter.php line 284

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace League\Flysystem\AwsS3V3;
  4. use Aws\Api\DateTimeResult;
  5. use Aws\S3\S3ClientInterface;
  6. use Generator;
  7. use League\Flysystem\Config;
  8. use League\Flysystem\DirectoryAttributes;
  9. use League\Flysystem\FileAttributes;
  10. use League\Flysystem\FilesystemAdapter;
  11. use League\Flysystem\FilesystemOperationFailed;
  12. use League\Flysystem\PathPrefixer;
  13. use League\Flysystem\StorageAttributes;
  14. use League\Flysystem\UnableToCheckFileExistence;
  15. use League\Flysystem\UnableToCopyFile;
  16. use League\Flysystem\UnableToDeleteFile;
  17. use League\Flysystem\UnableToMoveFile;
  18. use League\Flysystem\UnableToReadFile;
  19. use League\Flysystem\UnableToRetrieveMetadata;
  20. use League\Flysystem\UnableToSetVisibility;
  21. use League\Flysystem\UnableToWriteFile;
  22. use League\Flysystem\Visibility;
  23. use League\MimeTypeDetection\FinfoMimeTypeDetector;
  24. use League\MimeTypeDetection\MimeTypeDetector;
  25. use Psr\Http\Message\StreamInterface;
  26. use Throwable;
  27. class AwsS3V3Adapter implements FilesystemAdapter
  28. {
  29. /**
  30. * @var string[]
  31. */
  32. public const AVAILABLE_OPTIONS = [
  33. 'ACL',
  34. 'CacheControl',
  35. 'ContentDisposition',
  36. 'ContentEncoding',
  37. 'ContentLength',
  38. 'ContentType',
  39. 'Expires',
  40. 'GrantFullControl',
  41. 'GrantRead',
  42. 'GrantReadACP',
  43. 'GrantWriteACP',
  44. 'Metadata',
  45. 'MetadataDirective',
  46. 'RequestPayer',
  47. 'SSECustomerAlgorithm',
  48. 'SSECustomerKey',
  49. 'SSECustomerKeyMD5',
  50. 'SSEKMSKeyId',
  51. 'ServerSideEncryption',
  52. 'StorageClass',
  53. 'Tagging',
  54. 'WebsiteRedirectLocation',
  55. ];
  56. /**
  57. * @var string[]
  58. */
  59. private const EXTRA_METADATA_FIELDS = [
  60. 'Metadata',
  61. 'StorageClass',
  62. 'ETag',
  63. 'VersionId',
  64. ];
  65. /**
  66. * @var S3ClientInterface
  67. */
  68. private $client;
  69. /**
  70. * @var PathPrefixer
  71. */
  72. private $prefixer;
  73. /**
  74. * @var string
  75. */
  76. private $bucket;
  77. /**
  78. * @var VisibilityConverter
  79. */
  80. private $visibility;
  81. /**
  82. * @var MimeTypeDetector
  83. */
  84. private $mimeTypeDetector;
  85. /**
  86. * @var array
  87. */
  88. private $options;
  89. /**
  90. * @var bool
  91. */
  92. private $streamReads;
  93. public function __construct(
  94. S3ClientInterface $client,
  95. string $bucket,
  96. string $prefix = '',
  97. VisibilityConverter $visibility = null,
  98. MimeTypeDetector $mimeTypeDetector = null,
  99. array $options = [],
  100. bool $streamReads = true
  101. ) {
  102. $this->client = $client;
  103. $this->prefixer = new PathPrefixer($prefix);
  104. $this->bucket = $bucket;
  105. $this->visibility = $visibility ?: new PortableVisibilityConverter();
  106. $this->mimeTypeDetector = $mimeTypeDetector ?: new FinfoMimeTypeDetector();
  107. $this->options = $options;
  108. $this->streamReads = $streamReads;
  109. }
  110. public function fileExists(string $path): bool
  111. {
  112. try {
  113. return $this->client->doesObjectExist($this->bucket, $this->prefixer->prefixPath($path), $this->options);
  114. } catch (Throwable $exception) {
  115. throw UnableToCheckFileExistence::forLocation($path, $exception);
  116. }
  117. }
  118. public function write(string $path, string $contents, Config $config): void
  119. {
  120. $this->upload($path, $contents, $config);
  121. }
  122. /**
  123. * @param string $path
  124. * @param string|resource $body
  125. * @param Config $config
  126. */
  127. private function upload(string $path, $body, Config $config): void
  128. {
  129. $key = $this->prefixer->prefixPath($path);
  130. $options = $this->createOptionsFromConfig($config);
  131. $acl = $options['ACL'] ?? $this->determineAcl($config);
  132. $shouldDetermineMimetype = $body !== '' && ! array_key_exists('ContentType', $options);
  133. if ($shouldDetermineMimetype && $mimeType = $this->mimeTypeDetector->detectMimeType($key, $body)) {
  134. $options['ContentType'] = $mimeType;
  135. }
  136. try {
  137. $this->client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
  138. } catch (Throwable $exception) {
  139. throw UnableToWriteFile::atLocation($path, '', $exception);
  140. }
  141. }
  142. private function determineAcl(Config $config): string
  143. {
  144. $visibility = (string) $config->get(Config::OPTION_VISIBILITY, Visibility::PRIVATE);
  145. return $this->visibility->visibilityToAcl($visibility);
  146. }
  147. private function createOptionsFromConfig(Config $config): array
  148. {
  149. $options = [];
  150. if ($mimetype = $config->get('mimetype')) {
  151. $options['ContentType'] = $mimetype;
  152. }
  153. foreach (static::AVAILABLE_OPTIONS as $option) {
  154. $value = $config->get($option, '__NOT_SET__');
  155. if ($value !== '__NOT_SET__') {
  156. $options[$option] = $value;
  157. }
  158. }
  159. return $options + $this->options;
  160. }
  161. public function writeStream(string $path, $contents, Config $config): void
  162. {
  163. $this->upload($path, $contents, $config);
  164. }
  165. public function read(string $path): string
  166. {
  167. $body = $this->readObject($path, false);
  168. return (string) $body->getContents();
  169. }
  170. public function readStream(string $path)
  171. {
  172. /** @var resource $resource */
  173. $resource = $this->readObject($path, true)->detach();
  174. return $resource;
  175. }
  176. public function delete(string $path): void
  177. {
  178. $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];
  179. $command = $this->client->getCommand('DeleteObject', $arguments);
  180. try {
  181. $this->client->execute($command);
  182. } catch (Throwable $exception) {
  183. throw UnableToDeleteFile::atLocation($path, '', $exception);
  184. }
  185. }
  186. public function deleteDirectory(string $path): void
  187. {
  188. $prefix = $this->prefixer->prefixPath($path);
  189. $prefix = ltrim(rtrim($prefix, '/') . '/', '/');
  190. $this->client->deleteMatchingObjects($this->bucket, $prefix);
  191. }
  192. public function createDirectory(string $path, Config $config): void
  193. {
  194. $config = $config->withDefaults(['visibility' => $this->visibility->defaultForDirectories()]);
  195. $this->upload(rtrim($path, '/') . '/', '', $config);
  196. }
  197. public function setVisibility(string $path, string $visibility): void
  198. {
  199. $arguments = [
  200. 'Bucket' => $this->bucket,
  201. 'Key' => $this->prefixer->prefixPath($path),
  202. 'ACL' => $this->visibility->visibilityToAcl($visibility),
  203. ];
  204. $command = $this->client->getCommand('PutObjectAcl', $arguments);
  205. try {
  206. $this->client->execute($command);
  207. } catch (Throwable $exception) {
  208. throw UnableToSetVisibility::atLocation($path, '', $exception);
  209. }
  210. }
  211. public function visibility(string $path): FileAttributes
  212. {
  213. $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];
  214. $command = $this->client->getCommand('GetObjectAcl', $arguments);
  215. try {
  216. $result = $this->client->execute($command);
  217. } catch (Throwable $exception) {
  218. throw UnableToRetrieveMetadata::visibility($path, '', $exception);
  219. }
  220. $visibility = $this->visibility->aclToVisibility((array) $result->get('Grants'));
  221. return new FileAttributes($path, null, $visibility);
  222. }
  223. private function fetchFileMetadata(string $path, string $type): FileAttributes
  224. {
  225. $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];
  226. $command = $this->client->getCommand('HeadObject', $arguments);
  227. try {
  228. $result = $this->client->execute($command);
  229. } catch (Throwable $exception) {
  230. throw UnableToRetrieveMetadata::create($path, $type, '', $exception);
  231. }
  232. $attributes = $this->mapS3ObjectMetadata($result->toArray(), $path);
  233. if ( ! $attributes instanceof FileAttributes) {
  234. throw UnableToRetrieveMetadata::create($path, $type, '');
  235. }
  236. return $attributes;
  237. }
  238. private function mapS3ObjectMetadata(array $metadata, string $path = null): StorageAttributes
  239. {
  240. if ($path === null) {
  241. $path = $this->prefixer->stripPrefix($metadata['Key'] ?? $metadata['Prefix']);
  242. }
  243. if (substr($path, -1) === '/') {
  244. return new DirectoryAttributes(rtrim($path, '/'));
  245. }
  246. $mimetype = $metadata['ContentType'] ?? null;
  247. $fileSize = $metadata['ContentLength'] ?? $metadata['Size'] ?? null;
  248. $fileSize = $fileSize === null ? null : (int) $fileSize;
  249. $dateTime = $metadata['LastModified'] ?? null;
  250. $lastModified = $dateTime instanceof DateTimeResult ? $dateTime->getTimeStamp() : null;
  251. return new FileAttributes(
  252. $path,
  253. $fileSize,
  254. null,
  255. $lastModified,
  256. $mimetype,
  257. $this->extractExtraMetadata($metadata)
  258. );
  259. }
  260. private function extractExtraMetadata(array $metadata): array
  261. {
  262. $extracted = [];
  263. foreach (static::EXTRA_METADATA_FIELDS as $field) {
  264. if (isset($metadata[$field]) && $metadata[$field] !== '') {
  265. $extracted[$field] = $metadata[$field];
  266. }
  267. }
  268. return $extracted;
  269. }
  270. public function mimeType(string $path): FileAttributes
  271. {
  272. $attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_MIME_TYPE);
  273. if ($attributes->mimeType() === null) {
  274. throw UnableToRetrieveMetadata::mimeType($path);
  275. }
  276. return $attributes;
  277. }
  278. public function lastModified(string $path): FileAttributes
  279. {
  280. $attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_LAST_MODIFIED);
  281. if ($attributes->lastModified() === null) {
  282. throw UnableToRetrieveMetadata::lastModified($path);
  283. }
  284. return $attributes;
  285. }
  286. public function fileSize(string $path): FileAttributes
  287. {
  288. $attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_FILE_SIZE);
  289. if ($attributes->fileSize() === null) {
  290. throw UnableToRetrieveMetadata::fileSize($path);
  291. }
  292. return $attributes;
  293. }
  294. public function listContents(string $path, bool $deep): iterable
  295. {
  296. $prefix = trim($this->prefixer->prefixPath($path), '/');
  297. $prefix = empty($prefix) ? '' : $prefix . '/';
  298. $options = ['Bucket' => $this->bucket, 'Prefix' => $prefix];
  299. if ($deep === false) {
  300. $options['Delimiter'] = '/';
  301. }
  302. $listing = $this->retrievePaginatedListing($options);
  303. foreach ($listing as $item) {
  304. yield $this->mapS3ObjectMetadata($item);
  305. }
  306. }
  307. private function retrievePaginatedListing(array $options): Generator
  308. {
  309. $resultPaginator = $this->client->getPaginator('ListObjects', $options + $this->options);
  310. foreach ($resultPaginator as $result) {
  311. yield from ($result->get('CommonPrefixes') ?: []);
  312. yield from ($result->get('Contents') ?: []);
  313. }
  314. }
  315. public function move(string $source, string $destination, Config $config): void
  316. {
  317. try {
  318. $this->copy($source, $destination, $config);
  319. $this->delete($source);
  320. } catch (FilesystemOperationFailed $exception) {
  321. throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);
  322. }
  323. }
  324. public function copy(string $source, string $destination, Config $config): void
  325. {
  326. try {
  327. /** @var string $visibility */
  328. $visibility = $config->get(Config::OPTION_VISIBILITY) ?: $this->visibility($source)->visibility();
  329. } catch (Throwable $exception) {
  330. throw UnableToCopyFile::fromLocationTo(
  331. $source,
  332. $destination,
  333. $exception
  334. );
  335. }
  336. try {
  337. $this->client->copy(
  338. $this->bucket,
  339. $this->prefixer->prefixPath($source),
  340. $this->bucket,
  341. $this->prefixer->prefixPath($destination),
  342. $this->visibility->visibilityToAcl($visibility),
  343. $this->createOptionsFromConfig($config)
  344. );
  345. } catch (Throwable $exception) {
  346. throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);
  347. }
  348. }
  349. private function readObject(string $path, bool $wantsStream): StreamInterface
  350. {
  351. $options = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];
  352. if ($wantsStream && $this->streamReads && ! isset($this->options['@http']['stream'])) {
  353. $options['@http']['stream'] = true;
  354. }
  355. $command = $this->client->getCommand('GetObject', $options + $this->options);
  356. try {
  357. return $this->client->execute($command)->get('Body');
  358. } catch (Throwable $exception) {
  359. throw UnableToReadFile::fromLocation($path, '', $exception);
  360. }
  361. }
  362. }