Project

General

Profile

« Previous | Next » 

Revision b146b9b3

Added by Christian McDonald almost 4 years ago

Fix Disks widget UI on UFS systems

```
PHP 7.4.22 | 10 parallel jobs
............................................................ 60/279 (21 %)
............................................................ 120/279 (43 %)
............................................................ 180/279 (64 %)
............................................................ 240/279 (86 %)
....................................... 279/279 (100 %)

Checked 279 files in 0.9 seconds
No syntax error found
```

View differences:

src/usr/local/pfSense/include/Services/Filesystem/DF.php
1
<?php
2
/*
3
 * DF.php
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6
 * Copyright (c) 2021 Rubicon Communications, LLC (Netgate)
7
 * All rights reserved.
8
 *
9
 * Licensed under the Apache License, Version 2.0 (the "License");
10
 * you may not use this file except in compliance with the License.
11
 * You may obtain a copy of the License at
12
 *
13
 * http://www.apache.org/licenses/LICENSE-2.0
14
 *
15
 * Unless required by applicable law or agreed to in writing, software
16
 * distributed under the License is distributed on an "AS IS" BASIS,
17
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
 * See the License for the specific language governing permissions and
19
 * limitations under the License.
20
 */
21

  
22
namespace pfSense\Services\Filesystem;
23

  
24
use mikehaertl\shellcommand\Command;
25

  
26
use Symfony\Contracts\Cache\ItemInterface;
27
use Symfony\Component\Cache\Adapter\AbstractAdapter;
28
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
29

  
30
// Pull in pfSense globals
31
require_once('globals.inc');
32

  
33
final class DF {
34
	private const DF_PATH = '/bin/df';
35

  
36
	private const DF_CACHE_KEY  = 'DF';
37

  
38
	private $cache = null;
39

  
40
	public function __construct(AbstractAdapter $cache = null) {
41
		global $g;
42

  
43
		if (is_null($cache) || (!($cache instanceof AbstractAdapter))) {
44
			$cache = new PhpFilesAdapter(
45
				$namespace = $g['product_name'],
46
				$defaultLifetime = 30,
47
				$directory = "{$g['tmp_path']}/symfony-cache"
48
			);
49
		}
50

  
51
		$this->cache = $cache;
52
	}
53

  
54
	public function getCache() {
55
		return $this->cache;
56
	}
57

  
58
	public function setCache(AbstractAdapter $cache) {
59
		$this->cache = $cache;
60

  
61
		return $this;
62
	}
63

  
64
	public function flushCache() {
65
		$this->getCache()->delete(self::DF_CACHE_KEY);
66
	}
67

  
68
	public function getDfData($flush = false) {
69
		if ($flush) {
70
			$this->flushCache();
71
		}
72

  
73
		return $this->getCache()->get(self::DF_CACHE_KEY, function (ItemInterface $item, &$save) {
74
			$item->expiresAfter(30);
75

  
76
			$cmd = new Command(self::DF_PATH);
77

  
78
			$cmd->addArg('--libxo=json')->addArg('-h')->addArg('-T');
79

  
80
			$cmd->execute();
81

  
82
			$save = ($cmd->getExitCode() === 0) && !empty($cmd->getOutput());
83

  
84
			$retArray = $save ? json_decode($cmd->getOutput(), true) : array();
85

  
86
			array_walk_recursive($retArray, function (&$x) { $x = trim($x); });
87

  
88
			return $retArray;
89
		});
90
	}
91
}
92

  
93
?>
src/usr/local/pfSense/include/Services/Filesystem/Filesystem.php
21 21

  
22 22
namespace pfSense\Services\Filesystem;
23 23

  
24
use Nette\Utils\Arrays;
25
use Nette\Utils\Strings;
24
use Nette\Utils\{
25
	Arrays,
26
	Strings
27
};
26 28

  
27 29
final class Filesystem {
30
	private $parent = null;
31

  
28 32
	private $filesystem = [];
29 33

  
30
	public function __construct(array $filesystem) {
31
		$this->filesystem = $filesystem;
34
	private $children = [];
35

  
36
	public function __construct(?Filesystem $parent, $filesystem) {
37
		$this->_setParent($parent);
38

  
39
		$this->_setFilesystem($filesystem);
40

  
41
		$this->_initChildren();
32 42
	}
33 43

  
34
	public function getProperty($key, $default = null) {
35
		return Arrays::Get($this->filesystem, $key, $default);
44
	public function getBasename() {
45
		$basename = $this->getPath();
46

  
47
		if ($this->hasParent()) {
48

  
49
			if (Strings::startsWith($this->getPath(), $this->getParentPath())) {
50

  
51
				$basename = Strings::substring($basename, Strings::length($this->getParentPath()));
52
			
53
			}
54

  
55
		}
56

  
57
		return $basename;
36 58
	}
37 59

  
38 60
	public function getName() {
39
		return $this->getProperty('name');
61
		return $this->_getProperty('name');
40 62
	}
41 63

  
42
	public function getType() {
43
		return $this->getProperty('type');
64
	public function hasParent() {
65
		$parent = $this->getParent();
66

  
67
		return (!is_null($parent) && ($parent instanceof self));
44 68
	}
45 69

  
46
	public function getUsedPercent() {
47
		return $this->getProperty('used-percent');
70
	public function getParent() {
71
		return $this->parent;
48 72
	}
49 73

  
50
	public function getPath() {
51
		return $this->getProperty('mounted-on');
74
	public function getParentPath() {
75
		return $this->hasParent() ? $this->getParent()->getPath() : null;
52 76
	}
53 77

  
54
	public function getUsed() {
55
		return $this->getProperty('used');
78
	public function getPath() {
79
		return $this->_getProperty('mounted-on');
56 80
	}
57 81

  
58 82
	public function getSize() {
59
		return $this->getProperty('blocks');
83
		return $this->_getProperty('blocks');
60 84
	}
61 85

  
62
	public function getParentPath() {
63
		return dirname($this->getPath(), 1);
86
	public function getType() {
87
		return $this->_getProperty('type');
64 88
	}
65 89

  
66
	public function getHtmlClass() {
67
		return Strings::webalize("root{$this->getPath()}");
90
	public function getUsed() {
91
		return $this->_getProperty('used');
68 92
	}
69 93

  
70
	public function getParentHtmlClass() {
71
		return Strings::webalize("root{$this->getParentPath()}");
94
	public function getUsedPercent() {
95
		return $this->_getProperty('used-percent');
96
	}
97

  
98
	public function getHtmlClass(string $prefix = null, $parentPath = false) : string {
99
		$parent = $this->getParent();
100

  
101
		$prefix = $this->hasParent() ? "{$parent->getHtmlClass($prefix)}-" : "{$prefix}root";
102

  
103
		$suffix = $parentPath ? null : $this->getBasename();
104

  
105
		return Strings::webalize("{$prefix}{$suffix}");
106
	}
107

  
108
	public function getParentHtmlClass(string $prefix = null) : string {
109
		return $this->getHtmlClass($prefix, true);
72 110
	}
73 111

  
74 112
	public function isRoot() {
75 113
		return Strings::compare($this->getPath(), '/');
76 114
	}
115

  
116
	public function getChildrenAndSelf() {
117
		$filesystems = [$this,];
118

  
119
		foreach ($this->getChildren() as $child) {
120
			$children = $child->getChildrenAndSelf();
121

  
122
			$filesystems = array_merge($filesystems, $children);
123
		}
124

  
125
		return $filesystems;
126
	}
127

  
128
	public function hasChildren() {
129
		return !empty($this->children);
130
	}
131

  
132
	public function getChildren() {
133
		if (empty($this->children)) {
134
			$this->_initChildren();
135
		}
136

  
137
		return $this->children;
138
	}
139

  
140
	private function _getProperty($key, $default = null) {
141
		return Arrays::get($this->filesystem, $key, $default);
142
	}
143

  
144
	private function _initChildren() {
145
		$this->children = array_map(function($child) {
146
			return $this->_getNewFilesystemObject($child);
147
		}, $this->_getProperty('children'));
148
	}
149

  
150
	private function _setFilesystem($filesystem) {
151
		$this->filesystem = $filesystem;
152

  
153
		return $this;
154
	}
155

  
156
	private function _setParent(?Filesystem $parent) {
157
		$this->parent = $parent;
158

  
159
		return $this;
160
	}
161

  
162
	private function _getNewFilesystemObject($filesystem) {
163
		return (new Filesystem($this, $filesystem));
164
	}
77 165
}
78 166

  
79 167
?>
src/usr/local/pfSense/include/Services/Filesystem/Filesystems.php
21 21

  
22 22
namespace pfSense\Services\Filesystem;
23 23

  
24
use Nette\Utils\Arrays;
25
use Nette\Utils\Strings;
24
use pfSense\Services\Filesystem\Provider\{
25
	AbstractProvider,
26
	SystemProvider
27
};
28

  
29
use Nette\Utils\{
30
	Arrays,
31
	Strings
32
};
26 33

  
27 34
final class Filesystems {
28 35
	private $filesystems = [];
29 36

  
30
	private $df = null;
37
	private $provider = null;
31 38

  
32
	public function __construct(DF $df = null) {
33
		if (is_null($df) || (!($df instanceof DF))) {
34
			$df = new DF();
39
	public function __construct(AbstractProvider $provider = null) {
40
		if (is_null($provider)
41
		    || (!($df instanceof AbstractProvider))) {
42
			$provider = new SystemProvider();
35 43
		}
36 44

  
37
		$this->df = $df;
45
		$this->setProvider($provider);
38 46

  
39 47
		$this->_initFilesystems();
40 48
	}
41 49

  
42
	public function getDf() {
43
		return $this->df;
44
	}
45

  
46
	public function setDf(Df $df) {
47
		$this->__construct($df);
50
	public function setProvider(AbstractProvider $provider) {
51
		$this->provider = $provider;
48 52

  
49 53
		return $this;
50 54
	}
51 55

  
52
	public function getFilesystems(...$types) {
53
		$types = (is_array($types[0])) ? $types[0] : $types;
56
	public function getProvider() {
57
		return $this->provider;
58
	}
54 59

  
55
		return array_values(array_filter($this->filesystems, function ($fs) use ($types) {
56
			return (empty($types) || in_array($fs->getProperty('type'), $types));
57
		}));
60
	public function flushProviderCache() {
61
		$this->getProvider()->flushCache();
58 62
	}
59 63

  
60 64
	public function getRootFilesystem() {
61
		foreach ($this->getFilesystems() as $fs) {
65
		foreach ($this->_getFilesystemsTree() as $fs) {
62 66
			if ($fs->isRoot()) {
63 67
				return $fs;
64 68
			}
......
70 74
	public function getNonRootFilesystems(...$types) {
71 75
		$types = (is_array($types[0])) ? $types[0] : $types;
72 76

  
73
		return array_values(array_filter($this->getFilesystems($types), function ($fs) {
77
		return array_filter($this->getFilesystemsFlattened($types), function ($fs) {
74 78
			return !$fs->isRoot();
75
		}));
79
		});
76 80
	}
77 81

  
78 82
	public function getMounts(...$types) {
79 83
		$types = (is_array($types[0])) ? $types[0] : $types;
80 84

  
81 85
		return array_map(function($fs) {
82
			return $fs->getProperty('mounted-on');
83
		}, $this->getFilesystems($types));
86
			return $fs->getPath();
87
		}, $this->getFilesystemsFlattened($types));
84 88
	}
85 89

  
86
	private function _initFilesystems() {
87
		$df = $this->getDf()->getDfData();
90
	public function getFilesystemsFlattened(...$types) {
91
		$types = (is_array($types[0])) ? $types[0] : $types;
92

  
93
		$filesystems = [];
88 94

  
89
		$filesystems = Arrays::get($df, ['storage-system-information', 'filesystem'], array());
95
		foreach($this->_getFilesystemsTree() as $filesystem) {
96
			$filtered = array_filter($filesystem->getChildrenAndSelf(), function($fs) use ($types) {
97
				return (empty($types) || in_array($fs->getType(), $types));
98
			});
90 99

  
91
		foreach ($filesystems as $filesystem) {
92
			$filesystem = $this->_getNewFilesystemObject($filesystem);
93
			
94
			$this->filesystems[$filesystem->getProperty('mounted-on')] = $filesystem;
100
			$filesystems = array_merge($filesystems, $filtered);
95 101
		}
96 102

  
97
		usort($this->filesystems, function ($fs1, $fs2) {
98
			return $fs1->getProperty('mounted-on') <=> $fs2->getProperty('mounted-on');
99
		});
103
		return $filesystems;
104
	}
105

  
106
	public function _getFilesystemsTree() {
107
		if (empty($this->filesystems)){
108
			$this->_initFilesystems();
109
		}
110

  
111
		return $this->filesystems;
112
	}
113

  
114
	private function _initFilesystems() {
115
		$this->filesystems = array_map(function($filesystem) {
116
			return $this->_getNewFilesystemObject($filesystem);
117
		}, $this->getProvider()->getFilesystems());
100 118
	}
101 119

  
102 120
	private function _getNewFilesystemObject($filesystem) {
103
		return (new Filesystem($filesystem));
121
		return (new Filesystem(null, $filesystem));
104 122
	}
105 123
}
106 124

  
src/usr/local/pfSense/include/Services/Filesystem/Provider/AbstractProvider.php
1
<?php
2
/*
3
 * AbstractProvider.php
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6
 * Copyright (c) 2021 Rubicon Communications, LLC (Netgate)
7
 * All rights reserved.
8
 *
9
 * Licensed under the Apache License, Version 2.0 (the "License");
10
 * you may not use this file except in compliance with the License.
11
 * You may obtain a copy of the License at
12
 *
13
 * http://www.apache.org/licenses/LICENSE-2.0
14
 *
15
 * Unless required by applicable law or agreed to in writing, software
16
 * distributed under the License is distributed on an "AS IS" BASIS,
17
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
 * See the License for the specific language governing permissions and
19
 * limitations under the License.
20
 */
21

  
22
namespace pfSense\Services\Filesystem\Provider;
23

  
24
use mikehaertl\shellcommand\Command;
25

  
26
use Nette\Utils\{
27
	Arrays,
28
	Strings
29
};
30

  
31
use Symfony\{
32
	Contracts\Cache\ItemInterface,
33
	Component\Cache\Adapter\AbstractAdapter,
34
	Component\Cache\Adapter\PhpFilesAdapter
35
};
36

  
37
abstract class AbstractProvider {
38
	private $cacheAdapter = null;
39

  
40
	public function __construct(AbstractAdapter $cacheAdapter = null) {
41
		if (is_null($cacheAdapter) || (!($cacheAdapter instanceof AbstractAdapter))) {
42
			require_once('globals.inc');
43

  
44
			global $g;
45

  
46
			$tmpPath = Arrays::get($g, 'tmp_path', '/tmp');
47

  
48
			$cacheAdapter = new PhpFilesAdapter(
49
				$namespace = 'filesystem',
50
				$defaultLifetime = 10,
51
				$directory = "{$tmpPath}/symfony-cache"
52
			);
53
		}
54

  
55
		$this->setCacheAdapter($cacheAdapter);
56
	}
57

  
58
	public function setCacheAdapter(AbstractAdapter $cacheAdapter) {
59
		$this->cacheAdapter = $cacheAdapter;
60

  
61
		return $this;
62
	}
63

  
64
	public function getCacheAdapter() {
65
		return $this->cacheAdapter;
66
	}
67

  
68
	abstract function getFilesystems();
69
}
70

  
71
?>
src/usr/local/pfSense/include/Services/Filesystem/Provider/SystemProvider.php
1
<?php
2
/*
3
 * SystemProvider.php
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6
 * Copyright (c) 2021 Rubicon Communications, LLC (Netgate)
7
 * All rights reserved.
8
 *
9
 * Licensed under the Apache License, Version 2.0 (the "License");
10
 * you may not use this file except in compliance with the License.
11
 * You may obtain a copy of the License at
12
 *
13
 * http://www.apache.org/licenses/LICENSE-2.0
14
 *
15
 * Unless required by applicable law or agreed to in writing, software
16
 * distributed under the License is distributed on an "AS IS" BASIS,
17
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
 * See the License for the specific language governing permissions and
19
 * limitations under the License.
20
 */
21

  
22
namespace pfSense\Services\Filesystem\Provider;
23

  
24
use mikehaertl\shellcommand\Command;
25

  
26
use Nette\Utils\{
27
	Arrays,
28
	Strings
29
};
30

  
31
use Symfony\{
32
	Contracts\Cache\ItemInterface,
33
	Component\Cache\Adapter\AbstractAdapter,
34
	Component\Cache\Adapter\PhpFilesAdapter
35
};
36

  
37
final class SystemProvider extends AbstractProvider {
38
	private const DF_BINARY_PATH = '/bin/df';
39

  
40
	public function getFilesystems() {
41
		$roots = [];
42

  
43
		foreach ($this->_queryFilesystemProvider() as $idx => $filesystem) {
44
			$path = Arrays::get($filesystem, 'mounted-on', null);
45

  
46
			$filesystems[$path] = $filesystem;
47

  
48
			$filesystems[$path]['children'] = [];
49

  
50
			$parent = $this->_getParentFilesystemPath($path);
51

  
52
			$filesystems[$parent]['children'][$path] = &Arrays::getRef($filesystems, $path);
53

  
54
			if (is_null($parent)) {
55
				$roots[$path] = $path;
56
			}
57
		}
58

  
59
		return array_filter($filesystems, function($filesystem) use ($roots) {
60
			return array_key_exists($filesystem, $roots);
61
		}, ARRAY_FILTER_USE_KEY);
62
	}
63

  
64
	public function flushCache() {
65
		$this->getCacheAdapter()->prune();
66
	}
67
	public function _getParentFilesystemPath($path) {
68
		$filesystems = $this->_queryFilesystemProvider();
69

  
70
		foreach (array_reverse($filesystems) as $filesystem) {
71
			$mount = Arrays::get($filesystem, 'mounted-on', null);
72

  
73
			if (!Strings::compare($path, $mount)
74
			    && Strings::startsWith($path, $mount)) {
75
				return $mount;
76
			}
77
		}
78
	}
79

  
80
	private function _queryFilesystemProvider() {
81
		$cacheKey = Strings::webalize(__METHOD__);
82

  
83
		return $this->getCacheAdapter()->get($cacheKey, function (ItemInterface $item, &$save) {
84
			$cmd = new Command(self::DF_BINARY_PATH);
85

  
86
			$cmd->addArg('--libxo=json')->addArg('-h')->addArg('-T');
87

  
88
			$cmd->execute();
89

  
90
			$save = ($cmd->getExitCode() === 0) && !empty($cmd->getOutput());
91

  
92
			$retArray = $save ? json_decode($cmd->getOutput(), true) : array();
93

  
94
			array_walk_recursive($retArray, function (&$x) { $x = trim($x); });
95

  
96
			$filesystems = Arrays::get($retArray, ['storage-system-information', 'filesystem'], array());
97

  
98
			usort($filesystems, function($a, $b) {
99
				return (Arrays::get($a, 'mounted-on', null) <=> Arrays::get($b, 'mounted-on', null));
100
			});
101

  
102
			return $filesystems;
103
		});
104
	}
105
}
106

  
107
?>
src/usr/local/www/widgets/include/disks.inc
21 21

  
22 22
require_once('vendor/autoload.php');
23 23

  
24
use pfSense\Services\Filesystem\DF;
25
use pfSense\Services\Filesystem\Filesystem;
26
use pfSense\Services\Filesystem\Filesystems;
24
use pfSense\Services\Filesystem\{
25
	Filesystem,
26
	Filesystems,
27
	Provider\SystemProvider,
28
};
27 29

  
28 30
use Nette\Utils\Html;
29 31

  
......
35 37
				'disk_filter' => null,
36 38
				'autoshow_threshold' => 75);
37 39

  
40
// Initialize the Filesystems service		
41
$filesystems = new Filesystems();
42

  
38 43
function disks_do_widget_settings_post($post, $user_settings) {
39
	global $disks_types, $disks_widget_defaults;
44
	global $disks_types, $disks_widget_defaults, $filesystems;
40 45

  
41 46
	// start with widget defaults
42 47
	$pconfig = $disks_widget_defaults;
......
57 62

  
58 63
		// Check if the posted disk_filter is valid, if so set it...
59 64
		if (!empty($disk_filter) && is_array($disk_filter)) {
60
			$valid_mounts = (new Filesystems())->getMounts($disks_types);
65
			$valid_mounts = $filesystems->getMounts($disks_types);
61 66

  
62 67
			// Filter out any invalid mounts
63 68
			$disk_filter = array_filter($disk_filter, function($mount) use ($valid_mounts) {
......
164 169
}
165 170

  
166 171
function disk_compose_root_filesystem_row() {
167
	$fs = (new Filesystems())->getRootFilesystem();
172
	global $filesystems;
173

  
174
	$fs = $filesystems->getRootFilesystem();
168 175

  
169 176
	$tr = Html::el('tr');
170 177

  
......
193 200
}
194 201

  
195 202
function disks_compose_widget_user_body($widget_config) {
196
	global $disks_types;
203
	global $disks_types, $filesystems;
197 204

  
198 205
	$has_user_row = false;
199 206

  
......
202 209
	$disk_filter = explode(',', $widget_config['disk_filter']);
203 210

  
204 211
	$autoshow_threshold = $widget_config['autoshow_threshold'];
205

  
206
	$fss = (new Filesystems())->getNonRootFilesystems($disks_types);
207 212
	
208
	foreach ($fss as $fs) {
213
	foreach ($filesystems->getNonRootFilesystems($disks_types) as $fs) {
209 214
		if (in_array($fs->getPath(), $disk_filter)
210 215
		    || (($autoshow_threshold != 0) && ($fs->getUsedPercent() >= $autoshow_threshold))) {
211 216
			$has_user_row = true;
......
239 244
}
240 245

  
241 246
function disks_compose_widget_tree_body() {
242
	global $disks_types;
247
	global $disks_types, $filesystems;
243 248

  
244 249
	$tbody = Html::el('tbody');
245 250

  
246 251
	$tbody->addHtml(disk_compose_root_filesystem_row());
247 252

  
248
	$fss = (new Filesystems())->getNonRootFilesystems($disks_types);
249

  
250
	foreach ($fss as $fs) {
253
	foreach ($filesystems->getNonRootFilesystems($disks_types) as $fs) {
251 254
		$tr = Html::el('tr');
252 255

  
253 256
		$tr->setAttribute('class', "treegrid-{$fs->getHtmlClass()} treegrid-parent-{$fs->getParentHtmlClass()}");
......
278 281
}
279 282

  
280 283
function disks_get_nonroot_filesystems() {
281
	global $disks_types;
284
	global $disks_types, $filesystems;
282 285

  
283
	return (new Filesystems())->getNonRootFilesystems($disks_types);
286
	return $filesystems->getNonRootFilesystems($disks_types);
284 287
}
285 288

  
286 289
function disks_cache_invalidate($force = false, $chancef = 0.1) {
290
	global $filesystems;
291

  
287 292
	if (((rand() / getrandmax()) < $chancef) || $force) {
288
		(new DF)->flushCache();
293
		$filesystems->flushProviderCache();
289 294
	}
290 295
}
291 296

  
src/usr/local/www/widgets/widgets/disks.widget.php
28 28
// Widget includes
29 29
require_once('/usr/local/www/widgets/include/disks.inc');
30 30

  
31
// Randomly invalidate the cache on page refresh...
32
disks_cache_invalidate(false, 0.1);
33

  
34 31
global $disks_widget_defaults;
35 32

  
36 33
$widgetkey = (isset($_POST['widgetkey'])) ? $_POST['widgetkey'] : $widgetkey;
......
38 35
// Now overide any defaults with user settings
39 36
$widget_config = array_replace($disks_widget_defaults, (array) $user_settings['widgets'][$widgetkey]);
40 37

  
38
// Randomly invalidate the cache, 25% chance.
39
disks_cache_invalidate(false, 0.25);
40

  
41 41
// Are we handling an ajax refresh?
42 42
if (isset($_POST['ajax'])) {
43
	disks_cache_invalidate(true);
44

  
45 43
	print(disks_compose_widget_table($widget_config));
46 44

  
47 45
	// We are done here...

Also available in: Unified diff