33namespace Icinga \Module \Feeds \Storage ;
44
55use Icinga \Application \Icinga ;
6+ use Icinga \Util \DirectoryIterator ;
67use 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 */
1114class 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 ;
0 commit comments