FolderStorage.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @package Grav\Framework\Flex
  5. *
  6. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  7. * @license MIT License; see LICENSE file for details.
  8. */
  9. namespace Grav\Framework\Flex\Storage;
  10. use Grav\Common\Filesystem\Folder;
  11. use Grav\Common\Grav;
  12. use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
  13. use RocketTheme\Toolbox\File\File;
  14. use InvalidArgumentException;
  15. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  16. /**
  17. * Class FolderStorage
  18. * @package Grav\Framework\Flex\Storage
  19. */
  20. class FolderStorage extends AbstractFilesystemStorage
  21. {
  22. /** @var string */
  23. protected $dataFolder;
  24. /** @var string */
  25. protected $dataPattern = '{FOLDER}/{KEY}/item';
  26. /** @var bool */
  27. protected $prefixed;
  28. /** @var bool */
  29. protected $indexed;
  30. /**
  31. * {@inheritdoc}
  32. */
  33. public function __construct(array $options)
  34. {
  35. if (!isset($options['folder'])) {
  36. throw new InvalidArgumentException("Argument \$options is missing 'folder'");
  37. }
  38. $this->initDataFormatter($options['formatter'] ?? []);
  39. $this->initOptions($options);
  40. // Make sure that the data folder exists.
  41. $folder = $this->resolvePath($this->dataFolder);
  42. if (!file_exists($folder)) {
  43. try {
  44. Folder::create($folder);
  45. } catch (\RuntimeException $e) {
  46. throw new \RuntimeException(sprintf('Flex: %s', $e->getMessage()));
  47. }
  48. }
  49. }
  50. /**
  51. * {@inheritdoc}
  52. * @see FlexStorageInterface::getExistingKeys()
  53. */
  54. public function getExistingKeys(): array
  55. {
  56. return $this->buildIndex();
  57. }
  58. /**
  59. * {@inheritdoc}
  60. * @see FlexStorageInterface::hasKey()
  61. */
  62. public function hasKey(string $key): bool
  63. {
  64. return $key && strpos($key, '@@') === false && file_exists($this->getPathFromKey($key));
  65. }
  66. /**
  67. * {@inheritdoc}
  68. * @see FlexStorageInterface::createRows()
  69. */
  70. public function createRows(array $rows): array
  71. {
  72. $list = [];
  73. foreach ($rows as $key => $row) {
  74. // Create new file and save it.
  75. $key = $this->getNewKey();
  76. $path = $this->getPathFromKey($key);
  77. $file = $this->getFile($path);
  78. $list[$key] = $this->saveFile($file, $row);
  79. }
  80. return $list;
  81. }
  82. /**
  83. * {@inheritdoc}
  84. * @see FlexStorageInterface::readRows()
  85. */
  86. public function readRows(array $rows, array &$fetched = null): array
  87. {
  88. $list = [];
  89. foreach ($rows as $key => $row) {
  90. if (null === $row || (!\is_object($row) && !\is_array($row))) {
  91. // Only load rows which haven't been loaded before.
  92. $key = (string)$key;
  93. if (!$this->hasKey($key)) {
  94. $list[$key] = null;
  95. } else {
  96. $path = $this->getPathFromKey($key);
  97. $file = $this->getFile($path);
  98. $list[$key] = $this->loadFile($file);
  99. }
  100. if (null !== $fetched) {
  101. $fetched[$key] = $list[$key];
  102. }
  103. } else {
  104. // Keep the row if it has been loaded.
  105. $list[$key] = $row;
  106. }
  107. }
  108. return $list;
  109. }
  110. /**
  111. * {@inheritdoc}
  112. * @see FlexStorageInterface::updateRows()
  113. */
  114. public function updateRows(array $rows): array
  115. {
  116. $list = [];
  117. foreach ($rows as $key => $row) {
  118. $key = (string)$key;
  119. if (!$this->hasKey($key)) {
  120. $list[$key] = null;
  121. } else {
  122. $path = $this->getPathFromKey($key);
  123. $file = $this->getFile($path);
  124. $list[$key] = $this->saveFile($file, $row);
  125. }
  126. }
  127. return $list;
  128. }
  129. /**
  130. * {@inheritdoc}
  131. * @see FlexStorageInterface::deleteRows()
  132. */
  133. public function deleteRows(array $rows): array
  134. {
  135. $list = [];
  136. foreach ($rows as $key => $row) {
  137. $key = (string)$key;
  138. if (!$this->hasKey($key)) {
  139. $list[$key] = null;
  140. } else {
  141. $path = $this->getPathFromKey($key);
  142. $file = $this->getFile($path);
  143. $list[$key] = $this->deleteFile($file);
  144. $storage = $this->getStoragePath($key);
  145. $media = $this->getMediaPath($key);
  146. $this->deleteFolder($storage, true);
  147. $media && $this->deleteFolder($media, true);
  148. }
  149. }
  150. return $list;
  151. }
  152. /**
  153. * {@inheritdoc}
  154. * @see FlexStorageInterface::replaceRows()
  155. */
  156. public function replaceRows(array $rows): array
  157. {
  158. $list = [];
  159. foreach ($rows as $key => $row) {
  160. $key = (string)$key;
  161. if (strpos($key, '@@')) {
  162. $key = $this->getNewKey();
  163. }
  164. $path = $this->getPathFromKey($key);
  165. $file = $this->getFile($path);
  166. $list[$key] = $this->saveFile($file, $row);
  167. }
  168. return $list;
  169. }
  170. /**
  171. * {@inheritdoc}
  172. * @see FlexStorageInterface::renameRow()
  173. */
  174. public function renameRow(string $src, string $dst): bool
  175. {
  176. if ($this->hasKey($dst)) {
  177. throw new \RuntimeException("Cannot rename object: key '{$dst}' is already taken");
  178. }
  179. if (!$this->hasKey($src)) {
  180. return false;
  181. }
  182. return $this->moveFolder($this->getMediaPath($src), $this->getMediaPath($dst));
  183. }
  184. /**
  185. * {@inheritdoc}
  186. * @see FlexStorageInterface::getStoragePath()
  187. */
  188. public function getStoragePath(string $key = null): string
  189. {
  190. if (null === $key) {
  191. $path = $this->dataFolder;
  192. } else {
  193. $path = sprintf($this->dataPattern, $this->dataFolder, $key, substr($key, 0, 2));
  194. }
  195. return $path;
  196. }
  197. /**
  198. * {@inheritdoc}
  199. * @see FlexStorageInterface::getMediaPath()
  200. */
  201. public function getMediaPath(string $key = null): string
  202. {
  203. return null !== $key ? \dirname($this->getStoragePath($key)) : $this->getStoragePath();
  204. }
  205. /**
  206. * Get filesystem path from the key.
  207. *
  208. * @param string $key
  209. * @return string
  210. */
  211. public function getPathFromKey(string $key): string
  212. {
  213. return sprintf($this->dataPattern, $this->dataFolder, $key, substr($key, 0, 2));
  214. }
  215. /**
  216. * @param File $file
  217. * @return array|null
  218. */
  219. protected function loadFile(File $file): ?array
  220. {
  221. if (!$file->exists()) {
  222. return null;
  223. }
  224. try {
  225. $content = (array)$file->content();
  226. if (isset($content[0])) {
  227. throw new \RuntimeException('Broken object file.');
  228. }
  229. } catch (\RuntimeException $e) {
  230. $content = ['__error' => $e->getMessage()];
  231. }
  232. return $content;
  233. }
  234. /**
  235. * @param File $file
  236. * @param array $data
  237. * @return array
  238. */
  239. protected function saveFile(File $file, array $data): array
  240. {
  241. try {
  242. $file->save($data);
  243. /** @var UniformResourceLocator $locator */
  244. $locator = Grav::instance()['locator'];
  245. if ($locator->isStream($file->filename())) {
  246. $locator->clearCache($file->filename());
  247. }
  248. } catch (\RuntimeException $e) {
  249. throw new \RuntimeException(sprintf('Flex saveFile(%s): %s', $file->filename(), $e->getMessage()));
  250. }
  251. return $data;
  252. }
  253. /**
  254. * @param File $file
  255. * @return array|string
  256. */
  257. protected function deleteFile(File $file)
  258. {
  259. try {
  260. $data = $file->content();
  261. $file->delete();
  262. /** @var UniformResourceLocator $locator */
  263. $locator = Grav::instance()['locator'];
  264. if ($locator->isStream($file->filename())) {
  265. $locator->clearCache($file->filename());
  266. }
  267. } catch (\RuntimeException $e) {
  268. throw new \RuntimeException(sprintf('Flex deleteFile(%s): %s', $file->filename(), $e->getMessage()));
  269. }
  270. return $data;
  271. }
  272. /**
  273. * @param string $src
  274. * @param string $dst
  275. * @return bool
  276. */
  277. protected function moveFolder(string $src, string $dst): bool
  278. {
  279. try {
  280. Folder::move($this->resolvePath($src), $this->resolvePath($dst));
  281. /** @var UniformResourceLocator $locator */
  282. $locator = Grav::instance()['locator'];
  283. if ($locator->isStream($src) || $locator->isStream($dst)) {
  284. $locator->clearCache();
  285. }
  286. } catch (\RuntimeException $e) {
  287. throw new \RuntimeException(sprintf('Flex moveFolder(%s, %s): %s', $src, $dst, $e->getMessage()));
  288. }
  289. return true;
  290. }
  291. /**
  292. * @param string $path
  293. * @param bool $include_target
  294. * @return bool
  295. */
  296. protected function deleteFolder(string $path, bool $include_target = false): bool
  297. {
  298. try {
  299. $success = Folder::delete($this->resolvePath($path), $include_target);
  300. /** @var UniformResourceLocator $locator */
  301. $locator = Grav::instance()['locator'];
  302. if ($locator->isStream($path)) {
  303. $locator->clearCache();
  304. }
  305. return $success;
  306. } catch (\RuntimeException $e) {
  307. throw new \RuntimeException(sprintf('Flex deleteFolder(%s): %s', $path, $e->getMessage()));
  308. }
  309. }
  310. /**
  311. * Get key from the filesystem path.
  312. *
  313. * @param string $path
  314. * @return string
  315. */
  316. protected function getKeyFromPath(string $path): string
  317. {
  318. return basename($path);
  319. }
  320. /**
  321. * Returns list of all stored keys in [key => timestamp] pairs.
  322. *
  323. * @return array
  324. */
  325. protected function buildIndex(): array
  326. {
  327. $path = $this->getStoragePath();
  328. if (!file_exists($path)) {
  329. return [];
  330. }
  331. if ($this->prefixed) {
  332. $list = $this->buildPrefixedIndexFromFilesystem($path);
  333. } else {
  334. $list = $this->buildIndexFromFilesystem($path);
  335. }
  336. ksort($list, SORT_NATURAL);
  337. return $list;
  338. }
  339. protected function buildIndexFromFilesystem($path)
  340. {
  341. $flags = \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS;
  342. $iterator = new \FilesystemIterator($path, $flags);
  343. $list = [];
  344. /** @var \SplFileInfo $info */
  345. foreach ($iterator as $filename => $info) {
  346. if (!$info->isDir()) {
  347. continue;
  348. }
  349. $key = $this->getKeyFromPath($filename);
  350. $filename = $this->getPathFromKey($key);
  351. $modified = is_file($filename) ? filemtime($filename) : null;
  352. if (null === $modified) {
  353. continue;
  354. }
  355. $list[$key] = [
  356. 'storage_key' => $key,
  357. 'storage_timestamp' => $modified
  358. ];
  359. }
  360. return $list;
  361. }
  362. protected function buildPrefixedIndexFromFilesystem($path)
  363. {
  364. $flags = \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS;
  365. $iterator = new \FilesystemIterator($path, $flags);
  366. $list = [];
  367. /** @var \SplFileInfo $info */
  368. foreach ($iterator as $filename => $info) {
  369. if (!$info->isDir() || strpos($info->getFilename(), '.') === 0) {
  370. continue;
  371. }
  372. $list[] = $this->buildIndexFromFilesystem($filename);
  373. }
  374. if (!$list) {
  375. return [];
  376. }
  377. return \count($list) > 1 ? array_merge(...$list) : $list[0];
  378. }
  379. /**
  380. * @return string
  381. */
  382. protected function getNewKey(): string
  383. {
  384. // Make sure that the file doesn't exist.
  385. do {
  386. $key = $this->generateKey();
  387. } while (file_exists($this->getPathFromKey($key)));
  388. return $key;
  389. }
  390. /**
  391. * @param array $options
  392. */
  393. protected function initOptions(array $options): void
  394. {
  395. $extension = $this->dataFormatter->getDefaultFileExtension();
  396. /** @var string $pattern */
  397. $pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern;
  398. $this->dataFolder = $options['folder'];
  399. $this->prefixed = (bool)($options['prefixed'] ?? strpos($pattern, '/{KEY:2}/'));
  400. $this->indexed = (bool)($options['indexed'] ?? false);
  401. $this->keyField = $options['key'] ?? 'storage_key';
  402. $pattern = preg_replace(['/{FOLDER}/', '/{KEY}/', '/{KEY:2}/'], ['%1$s', '%2$s', '%3$s'], $pattern);
  403. $this->dataPattern = \dirname($pattern) . '/' . basename($pattern, $extension) . $extension;
  404. }
  405. }