FileStorage.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. <?php
  2. namespace Drupal\Core\Config;
  3. use Drupal\Component\FileCache\FileCacheFactory;
  4. use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
  5. use Drupal\Core\Serialization\Yaml;
  6. /**
  7. * Defines the file storage.
  8. */
  9. class FileStorage implements StorageInterface {
  10. /**
  11. * The storage collection.
  12. *
  13. * @var string
  14. */
  15. protected $collection;
  16. /**
  17. * The filesystem path for configuration objects.
  18. *
  19. * @var string
  20. */
  21. protected $directory = '';
  22. /**
  23. * The file cache object.
  24. *
  25. * @var \Drupal\Component\FileCache\FileCacheInterface
  26. */
  27. protected $fileCache;
  28. /**
  29. * Constructs a new FileStorage.
  30. *
  31. * @param string $directory
  32. * A directory path to use for reading and writing of configuration files.
  33. * @param string $collection
  34. * (optional) The collection to store configuration in. Defaults to the
  35. * default collection.
  36. */
  37. public function __construct($directory, $collection = StorageInterface::DEFAULT_COLLECTION) {
  38. $this->directory = $directory;
  39. $this->collection = $collection;
  40. // Use a NULL File Cache backend by default. This will ensure only the
  41. // internal static caching of FileCache is used and thus avoids blowing up
  42. // the APCu cache.
  43. $this->fileCache = FileCacheFactory::get('config', ['cache_backend_class' => NULL]);
  44. }
  45. /**
  46. * Returns the path to the configuration file.
  47. *
  48. * @return string
  49. * The path to the configuration file.
  50. */
  51. public function getFilePath($name) {
  52. return $this->getCollectionDirectory() . '/' . $name . '.' . static::getFileExtension();
  53. }
  54. /**
  55. * Returns the file extension used by the file storage for all configuration files.
  56. *
  57. * @return string
  58. * The file extension.
  59. */
  60. public static function getFileExtension() {
  61. return 'yml';
  62. }
  63. /**
  64. * Check if the directory exists and create it if not.
  65. */
  66. protected function ensureStorage() {
  67. $dir = $this->getCollectionDirectory();
  68. $success = file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
  69. // Only create .htaccess file in root directory.
  70. if ($dir == $this->directory) {
  71. $success = $success && file_save_htaccess($this->directory, TRUE, TRUE);
  72. }
  73. if (!$success) {
  74. throw new StorageException('Failed to create config directory ' . $dir);
  75. }
  76. return $this;
  77. }
  78. /**
  79. * {@inheritdoc}
  80. */
  81. public function exists($name) {
  82. return file_exists($this->getFilePath($name));
  83. }
  84. /**
  85. * Implements Drupal\Core\Config\StorageInterface::read().
  86. *
  87. * @throws \Drupal\Core\Config\UnsupportedDataTypeConfigException
  88. */
  89. public function read($name) {
  90. if (!$this->exists($name)) {
  91. return FALSE;
  92. }
  93. $filepath = $this->getFilePath($name);
  94. if ($data = $this->fileCache->get($filepath)) {
  95. return $data;
  96. }
  97. $data = file_get_contents($filepath);
  98. try {
  99. $data = $this->decode($data);
  100. }
  101. catch (InvalidDataTypeException $e) {
  102. throw new UnsupportedDataTypeConfigException('Invalid data type in config ' . $name . ', found in file' . $filepath . ' : ' . $e->getMessage());
  103. }
  104. $this->fileCache->set($filepath, $data);
  105. return $data;
  106. }
  107. /**
  108. * {@inheritdoc}
  109. */
  110. public function readMultiple(array $names) {
  111. $list = [];
  112. foreach ($names as $name) {
  113. if ($data = $this->read($name)) {
  114. $list[$name] = $data;
  115. }
  116. }
  117. return $list;
  118. }
  119. /**
  120. * {@inheritdoc}
  121. */
  122. public function write($name, array $data) {
  123. try {
  124. $encoded_data = $this->encode($data);
  125. }
  126. catch (InvalidDataTypeException $e) {
  127. throw new StorageException("Invalid data type in config $name: {$e->getMessage()}");
  128. }
  129. $target = $this->getFilePath($name);
  130. $status = @file_put_contents($target, $encoded_data);
  131. if ($status === FALSE) {
  132. // Try to make sure the directory exists and try writing again.
  133. $this->ensureStorage();
  134. $status = @file_put_contents($target, $encoded_data);
  135. }
  136. if ($status === FALSE) {
  137. throw new StorageException('Failed to write configuration file: ' . $this->getFilePath($name));
  138. }
  139. else {
  140. drupal_chmod($target);
  141. }
  142. $this->fileCache->set($target, $data);
  143. return TRUE;
  144. }
  145. /**
  146. * {@inheritdoc}
  147. */
  148. public function delete($name) {
  149. if (!$this->exists($name)) {
  150. $dir = $this->getCollectionDirectory();
  151. if (!file_exists($dir)) {
  152. throw new StorageException($dir . '/ not found.');
  153. }
  154. return FALSE;
  155. }
  156. $this->fileCache->delete($this->getFilePath($name));
  157. return drupal_unlink($this->getFilePath($name));
  158. }
  159. /**
  160. * {@inheritdoc}
  161. */
  162. public function rename($name, $new_name) {
  163. $status = @rename($this->getFilePath($name), $this->getFilePath($new_name));
  164. if ($status === FALSE) {
  165. throw new StorageException('Failed to rename configuration file from: ' . $this->getFilePath($name) . ' to: ' . $this->getFilePath($new_name));
  166. }
  167. $this->fileCache->delete($this->getFilePath($name));
  168. $this->fileCache->delete($this->getFilePath($new_name));
  169. return TRUE;
  170. }
  171. /**
  172. * {@inheritdoc}
  173. */
  174. public function encode($data) {
  175. return Yaml::encode($data);
  176. }
  177. /**
  178. * {@inheritdoc}
  179. */
  180. public function decode($raw) {
  181. $data = Yaml::decode($raw);
  182. // A simple string is valid YAML for any reason.
  183. if (!is_array($data)) {
  184. return FALSE;
  185. }
  186. return $data;
  187. }
  188. /**
  189. * {@inheritdoc}
  190. */
  191. public function listAll($prefix = '') {
  192. $dir = $this->getCollectionDirectory();
  193. if (!is_dir($dir)) {
  194. return [];
  195. }
  196. $extension = '.' . static::getFileExtension();
  197. // glob() directly calls into libc glob(), which is not aware of PHP stream
  198. // wrappers. Same for \GlobIterator (which additionally requires an absolute
  199. // realpath() on Windows).
  200. // @see https://github.com/mikey179/vfsStream/issues/2
  201. $files = scandir($dir);
  202. $names = [];
  203. $pattern = '/^' . preg_quote($prefix, '/') . '.*' . preg_quote($extension, '/') . '$/';
  204. foreach ($files as $file) {
  205. if ($file[0] !== '.' && preg_match($pattern, $file)) {
  206. $names[] = basename($file, $extension);
  207. }
  208. }
  209. return $names;
  210. }
  211. /**
  212. * {@inheritdoc}
  213. */
  214. public function deleteAll($prefix = '') {
  215. $success = TRUE;
  216. $files = $this->listAll($prefix);
  217. foreach ($files as $name) {
  218. if (!$this->delete($name) && $success) {
  219. $success = FALSE;
  220. }
  221. }
  222. if ($success && $this->collection != StorageInterface::DEFAULT_COLLECTION) {
  223. // Remove empty directories.
  224. if (!(new \FilesystemIterator($this->getCollectionDirectory()))->valid()) {
  225. drupal_rmdir($this->getCollectionDirectory());
  226. }
  227. }
  228. return $success;
  229. }
  230. /**
  231. * {@inheritdoc}
  232. */
  233. public function createCollection($collection) {
  234. return new static(
  235. $this->directory,
  236. $collection
  237. );
  238. }
  239. /**
  240. * {@inheritdoc}
  241. */
  242. public function getCollectionName() {
  243. return $this->collection;
  244. }
  245. /**
  246. * {@inheritdoc}
  247. */
  248. public function getAllCollectionNames() {
  249. if (!is_dir($this->directory)) {
  250. return [];
  251. }
  252. $collections = $this->getAllCollectionNamesHelper($this->directory);
  253. sort($collections);
  254. return $collections;
  255. }
  256. /**
  257. * Helper function for getAllCollectionNames().
  258. *
  259. * If the file storage has the following subdirectory structure:
  260. * ./another_collection/one
  261. * ./another_collection/two
  262. * ./collection/sub/one
  263. * ./collection/sub/two
  264. * this function will return:
  265. * @code
  266. * array(
  267. * 'another_collection.one',
  268. * 'another_collection.two',
  269. * 'collection.sub.one',
  270. * 'collection.sub.two',
  271. * );
  272. * @endcode
  273. *
  274. * @param string $directory
  275. * The directory to check for sub directories. This allows this
  276. * function to be used recursively to discover all the collections in the
  277. * storage. It is the responsibility of the caller to ensure the directory
  278. * exists.
  279. *
  280. * @return array
  281. * A list of collection names contained within the provided directory.
  282. */
  283. protected function getAllCollectionNamesHelper($directory) {
  284. $collections = [];
  285. $pattern = '/\.' . preg_quote($this->getFileExtension(), '/') . '$/';
  286. foreach (new \DirectoryIterator($directory) as $fileinfo) {
  287. if ($fileinfo->isDir() && !$fileinfo->isDot()) {
  288. $collection = $fileinfo->getFilename();
  289. // Recursively call getAllCollectionNamesHelper() to discover if there
  290. // are subdirectories. Subdirectories represent a dotted collection
  291. // name.
  292. $sub_collections = $this->getAllCollectionNamesHelper($directory . '/' . $collection);
  293. if (!empty($sub_collections)) {
  294. // Build up the collection name by concatenating the subdirectory
  295. // names with the current directory name.
  296. foreach ($sub_collections as $sub_collection) {
  297. $collections[] = $collection . '.' . $sub_collection;
  298. }
  299. }
  300. // Check that the collection is valid by searching it for configuration
  301. // objects. A directory without any configuration objects is not a valid
  302. // collection.
  303. // @see \Drupal\Core\Config\FileStorage::listAll()
  304. foreach (scandir($directory . '/' . $collection) as $file) {
  305. if ($file[0] !== '.' && preg_match($pattern, $file)) {
  306. $collections[] = $collection;
  307. break;
  308. }
  309. }
  310. }
  311. }
  312. return $collections;
  313. }
  314. /**
  315. * Gets the directory for the collection.
  316. *
  317. * @return string
  318. * The directory for the collection.
  319. */
  320. protected function getCollectionDirectory() {
  321. if ($this->collection == StorageInterface::DEFAULT_COLLECTION) {
  322. $dir = $this->directory;
  323. }
  324. else {
  325. $dir = $this->directory . '/' . str_replace('.', '/', $this->collection);
  326. }
  327. return $dir;
  328. }
  329. }