Skip to content

Commit e9b49bd

Browse files
committed
Refactor FilesystemStorage to use single files per feed
The rationale behind this is to make shipping configuration with a installation package simpler. So that users can package the module with custom feeds and not override configuration upon updates.
1 parent 21f0933 commit e9b49bd

2 files changed

Lines changed: 67 additions & 44 deletions

File tree

library/Feeds/Storage/FilesystemStorage.php

Lines changed: 66 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,34 @@
33
namespace Icinga\Module\Feeds\Storage;
44

55
use Icinga\Application\Icinga;
6+
use Icinga\Util\DirectoryIterator;
67
use Icinga\Exception\SystemPermissionException;
8+
use Icinga\Exception\NotWritableError;
9+
use Icinga\Exception\NotReadableError;
710

811
/**
912
* FilesystemStorage is used to store the feeds configuration locally
1013
*/
1114
class FilesystemStorage implements StorageInterface
1215
{
13-
const FILE_NAME = "feeds.json";
16+
const FILE_SUFFIX = ".json";
1417
const JSON_FLAGS = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
1518
const VERSION = 1;
1619

1720
protected array $feeds = [];
1821
protected bool $loaded = false;
1922

20-
protected function getConfigFile(): string
23+
protected function getConfigDir(): string
2124
{
2225
return Icinga::app()
2326
->getModuleManager()
2427
->getModule('feeds')
25-
->getConfigDir() . DIRECTORY_SEPARATOR . self::FILE_NAME;
28+
->getConfigDir();
2629
}
2730

2831
protected function ensureConfigDir(): void
2932
{
30-
$file = $this->getConfigFile();
31-
$dir = dirname($file);
33+
$dir = $this->getConfigDir();
3234

3335
if (!is_dir($dir)) {
3436
if (!is_dir(dirname($dir))) {
@@ -42,20 +44,57 @@ protected function ensureConfigDir(): void
4244
}
4345
}
4446

45-
protected function ensureConfigFile(): void
47+
protected function loadFeedFile(string $filename): FeedDefinition
4648
{
47-
$file = $this->getConfigFile();
48-
$this->ensureConfigDir();
49+
$filePath = $this->getConfigDir() . DIRECTORY_SEPARATOR . $filename;
50+
51+
if (!is_readable($filePath)) {
52+
throw new NotReadableError('Could not read file %s', $filePath);
53+
}
54+
55+
$data = file_get_contents($filePath);
56+
57+
if ($data === false) {
58+
throw new NotReadableError('Could not read file %s', $filePath);
59+
}
60+
61+
$json = json_decode($data, true);
62+
$feed = FeedDefinition::fromArray($json);
63+
64+
return $feed;
65+
}
4966

50-
if (!is_file($file)) {
51-
$data = ['version' => self::VERSION];
67+
protected function storeFeedFile(FeedDefinition $feed): void
68+
{
69+
$filePath = $this->getConfigDir() . DIRECTORY_SEPARATOR . $feed->name . self::FILE_SUFFIX;
70+
71+
$exists = file_exists($filePath);
72+
$content = json_encode($feed->toArray(), static::JSON_FLAGS);
5273

53-
if (file_put_contents($file, json_encode($data, static::JSON_FLAGS)) === false) {
54-
throw new SystemPermissionException('Could not write config file "%s"', dirname($file));
74+
if (file_put_contents($filePath, $content) === false) {
75+
throw new NotWritableError('Could not save to %s', $filePath);
76+
}
77+
78+
// If this is a new file, we make sure to set the mode
79+
if ($exists === false) {
80+
$fileMode = intval('0660', 8);
81+
if (false === @chmod($filePath, $fileMode)) {
82+
throw new NotWritableError('Failed to set file mode "0660" on file "%s"', $filePath);
5583
}
5684
}
5785
}
5886

87+
public function removeFeedFile(string $filename): bool
88+
{
89+
$filePath = $this->getConfigDir() . DIRECTORY_SEPARATOR . $filename . self::FILE_SUFFIX;
90+
91+
if (file_exists($filePath)) {
92+
return unlink($filePath);
93+
}
94+
95+
return true;
96+
}
97+
5998
public function getFeeds(): array
6099
{
61100
$this->load();
@@ -70,16 +109,15 @@ public function getFeedByName(string $name): ?FeedDefinition
70109

71110
public function removeFeed(string|FeedDefinition $feed): bool
72111
{
112+
// TODO: This won't work when the feedname and filename don't match
73113
if (!is_string($feed)) {
74-
return $this->removeFeed($feed->name);
114+
return $this->removeFeedFile($feed->name);
75115
}
76116

77117
if (!$this->getFeedByName($feed)) {
78118
return false;
79119
}
80120

81-
unset($this->feeds[$feed]);
82-
83121
$this->flush();
84122

85123
return true;
@@ -107,16 +145,11 @@ public function flush(): void
107145
'feeds' => []
108146
];
109147

148+
$this->ensureConfigDir();
149+
110150
foreach ($this->getFeeds() as $feed) {
111151
$data['feeds'][] = $feed->toArray();
112-
}
113-
114-
$this->ensureConfigFile();
115-
116-
$file = $this->getConfigFile();
117-
118-
if (file_put_contents($file, json_encode($data, static::JSON_FLAGS)) === false) {
119-
throw new SystemPermissionException('Could not write config file "%s"', dirname($file));
152+
$this->storeFeedFile($feed);
120153
}
121154
}
122155

@@ -127,29 +160,19 @@ protected function load(): void
127160
}
128161

129162
$this->feeds = [];
163+
$this->ensureConfigDir();
130164

131-
$this->ensureConfigFile();
132-
$rawData = file_get_contents($this->getConfigFile());
133-
134-
if ($rawData === false) {
135-
throw new SystemPermissionException('Could not read config file "%s"', $this->getConfigFile());
136-
}
137-
138-
$json = json_decode($rawData, true);
139-
140-
if ($json === null) {
141-
throw new SystemPermissionException('Could not read config file "%s"', $this->getConfigFile());
142-
}
143-
144-
if (!array_key_exists('version', $json)) {
145-
throw new SystemPermissionException("Config file doesn't contain a version number. File: %s", $this->getConfigFile());
146-
}
165+
// Load the JSON files for the feeds from the config directory
166+
$directory = new DirectoryIterator($this->getConfigDir(), self::FILE_SUFFIX);
147167

148-
if (array_key_exists('feeds', $json)) {
149-
foreach ($json['feeds'] as $feedData) {
150-
$feed = FeedDefinition::fromArray($feedData);
151-
$this->feeds[$feed->name] = $feed;
168+
foreach ($directory as $name => $path) {
169+
if (is_dir($path)) {
170+
// Do not descend and ignore directories
171+
continue;
152172
}
173+
174+
$feed = $this->loadFeedFile($name);
175+
$this->feeds[$feed->name] = $feed;
153176
}
154177

155178
$this->loaded = true;

library/Feeds/Storage/StorageFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class StorageFactory
88

99
protected static function getStorageImpl(): StorageInterface
1010
{
11-
// TODO: Once there are multiple storage options they can be determined
11+
// NOTE: Once there are multiple storage options they can be determined
1212
// from the config file
1313
return new FilesystemStorage();
1414
}

0 commit comments

Comments
 (0)