3 namespace Drupal\Core\Config;
5 use Drupal\Component\FileCache\FileCacheFactory;
6 use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
7 use Drupal\Core\Serialization\Yaml;
10 * Defines the file storage.
12 class FileStorage implements StorageInterface {
15 * The storage collection.
19 protected $collection;
22 * The filesystem path for configuration objects.
26 protected $directory = '';
29 * The file cache object.
31 * @var \Drupal\Component\FileCache\FileCacheInterface
36 * Constructs a new FileStorage.
38 * @param string $directory
39 * A directory path to use for reading and writing of configuration files.
40 * @param string $collection
41 * (optional) The collection to store configuration in. Defaults to the
44 public function __construct($directory, $collection = StorageInterface::DEFAULT_COLLECTION) {
45 $this->directory = $directory;
46 $this->collection = $collection;
48 // Use a NULL File Cache backend by default. This will ensure only the
49 // internal static caching of FileCache is used and thus avoids blowing up
51 $this->fileCache = FileCacheFactory::get('config', ['cache_backend_class' => NULL]);
55 * Returns the path to the configuration file.
58 * The path to the configuration file.
60 public function getFilePath($name) {
61 return $this->getCollectionDirectory() . '/' . $name . '.' . static::getFileExtension();
65 * Returns the file extension used by the file storage for all configuration files.
70 public static function getFileExtension() {
75 * Check if the directory exists and create it if not.
77 protected function ensureStorage() {
78 $dir = $this->getCollectionDirectory();
79 $success = file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
80 // Only create .htaccess file in root directory.
81 if ($dir == $this->directory) {
82 $success = $success && file_save_htaccess($this->directory, TRUE, TRUE);
85 throw new StorageException('Failed to create config directory ' . $dir);
93 public function exists($name) {
94 return file_exists($this->getFilePath($name));
98 * Implements Drupal\Core\Config\StorageInterface::read().
100 * @throws \Drupal\Core\Config\UnsupportedDataTypeConfigException
102 public function read($name) {
103 if (!$this->exists($name)) {
107 $filepath = $this->getFilePath($name);
108 if ($data = $this->fileCache->get($filepath)) {
112 $data = file_get_contents($filepath);
114 $data = $this->decode($data);
116 catch (InvalidDataTypeException $e) {
117 throw new UnsupportedDataTypeConfigException('Invalid data type in config ' . $name . ', found in file' . $filepath . ' : ' . $e->getMessage());
119 $this->fileCache->set($filepath, $data);
127 public function readMultiple(array $names) {
129 foreach ($names as $name) {
130 if ($data = $this->read($name)) {
131 $list[$name] = $data;
140 public function write($name, array $data) {
142 $encoded_data = $this->encode($data);
144 catch (InvalidDataTypeException $e) {
145 throw new StorageException("Invalid data type in config $name: {$e->getMessage()}");
148 $target = $this->getFilePath($name);
149 $status = @file_put_contents($target, $encoded_data);
150 if ($status === FALSE) {
151 // Try to make sure the directory exists and try writing again.
152 $this->ensureStorage();
153 $status = @file_put_contents($target, $encoded_data);
155 if ($status === FALSE) {
156 throw new StorageException('Failed to write configuration file: ' . $this->getFilePath($name));
159 drupal_chmod($target);
162 $this->fileCache->set($target, $data);
170 public function delete($name) {
171 if (!$this->exists($name)) {
172 $dir = $this->getCollectionDirectory();
173 if (!file_exists($dir)) {
174 throw new StorageException($dir . '/ not found.');
178 $this->fileCache->delete($this->getFilePath($name));
179 return drupal_unlink($this->getFilePath($name));
185 public function rename($name, $new_name) {
186 $status = @rename($this->getFilePath($name), $this->getFilePath($new_name));
187 if ($status === FALSE) {
188 throw new StorageException('Failed to rename configuration file from: ' . $this->getFilePath($name) . ' to: ' . $this->getFilePath($new_name));
190 $this->fileCache->delete($this->getFilePath($name));
191 $this->fileCache->delete($this->getFilePath($new_name));
198 public function encode($data) {
199 return Yaml::encode($data);
205 public function decode($raw) {
206 $data = Yaml::decode($raw);
207 // A simple string is valid YAML for any reason.
208 if (!is_array($data)) {
217 public function listAll($prefix = '') {
218 $dir = $this->getCollectionDirectory();
222 $extension = '.' . static::getFileExtension();
224 // glob() directly calls into libc glob(), which is not aware of PHP stream
225 // wrappers. Same for \GlobIterator (which additionally requires an absolute
226 // realpath() on Windows).
227 // @see https://github.com/mikey179/vfsStream/issues/2
228 $files = scandir($dir);
231 $pattern = '/^' . preg_quote($prefix, '/') . '.*' . preg_quote($extension, '/') . '$/';
232 foreach ($files as $file) {
233 if ($file[0] !== '.' && preg_match($pattern, $file)) {
234 $names[] = basename($file, $extension);
244 public function deleteAll($prefix = '') {
246 $files = $this->listAll($prefix);
247 foreach ($files as $name) {
248 if (!$this->delete($name) && $success) {
252 if ($success && $this->collection != StorageInterface::DEFAULT_COLLECTION) {
253 // Remove empty directories.
254 if (!(new \FilesystemIterator($this->getCollectionDirectory()))->valid()) {
255 drupal_rmdir($this->getCollectionDirectory());
264 public function createCollection($collection) {
274 public function getCollectionName() {
275 return $this->collection;
281 public function getAllCollectionNames() {
282 if (!is_dir($this->directory)) {
285 $collections = $this->getAllCollectionNamesHelper($this->directory);
291 * Helper function for getAllCollectionNames().
293 * If the file storage has the following subdirectory structure:
294 * ./another_collection/one
295 * ./another_collection/two
296 * ./collection/sub/one
297 * ./collection/sub/two
298 * this function will return:
301 * 'another_collection.one',
302 * 'another_collection.two',
303 * 'collection.sub.one',
304 * 'collection.sub.two',
308 * @param string $directory
309 * The directory to check for sub directories. This allows this
310 * function to be used recursively to discover all the collections in the
311 * storage. It is the responsibility of the caller to ensure the directory
315 * A list of collection names contained within the provided directory.
317 protected function getAllCollectionNamesHelper($directory) {
319 $pattern = '/\.' . preg_quote($this->getFileExtension(), '/') . '$/';
320 foreach (new \DirectoryIterator($directory) as $fileinfo) {
321 if ($fileinfo->isDir() && !$fileinfo->isDot()) {
322 $collection = $fileinfo->getFilename();
323 // Recursively call getAllCollectionNamesHelper() to discover if there
324 // are subdirectories. Subdirectories represent a dotted collection
326 $sub_collections = $this->getAllCollectionNamesHelper($directory . '/' . $collection);
327 if (!empty($sub_collections)) {
328 // Build up the collection name by concatenating the subdirectory
329 // names with the current directory name.
330 foreach ($sub_collections as $sub_collection) {
331 $collections[] = $collection . '.' . $sub_collection;
334 // Check that the collection is valid by searching it for configuration
335 // objects. A directory without any configuration objects is not a valid
337 // @see \Drupal\Core\Config\FileStorage::listAll()
338 foreach (scandir($directory . '/' . $collection) as $file) {
339 if ($file[0] !== '.' && preg_match($pattern, $file)) {
340 $collections[] = $collection;
350 * Gets the directory for the collection.
353 * The directory for the collection.
355 protected function getCollectionDirectory() {
356 if ($this->collection == StorageInterface::DEFAULT_COLLECTION) {
357 $dir = $this->directory;
360 $dir = $this->directory . '/' . str_replace('.', '/', $this->collection);