feat: add bounded file inventory for media audits
This commit is contained in:
@@ -234,6 +234,96 @@ function shares_list(?string $requested): array {
|
||||
return ['schema_version' => '1.0', 'share_count' => count($items), 'shares' => $items];
|
||||
}
|
||||
|
||||
function files_inventory(
|
||||
string $share,
|
||||
string $relative_path,
|
||||
string $name_contains,
|
||||
int $max_depth,
|
||||
int $max_entries,
|
||||
bool $include_files,
|
||||
bool $include_directories
|
||||
): array {
|
||||
$known = [];
|
||||
foreach (ini_sections('/var/local/emhttp/shares.ini') as $section => $entry) {
|
||||
$name = (string)($entry['name'] ?? trim($section, '"'));
|
||||
if ($name !== '') $known[$name] = true;
|
||||
}
|
||||
if (!isset($known[$share])) fail_status('Unknown share name');
|
||||
if ($relative_path !== '' && ($relative_path[0] === '/' || str_contains($relative_path, "\0"))) {
|
||||
fail_status('relative_path must stay below the selected share');
|
||||
}
|
||||
$parts = array_values(array_filter(explode('/', str_replace('\\', '/', $relative_path)), fn($p) => $p !== ''));
|
||||
if (in_array('..', $parts, true) || in_array('.', $parts, true)) {
|
||||
fail_status('relative_path traversal is not allowed');
|
||||
}
|
||||
$relative_path = implode('/', $parts);
|
||||
$root = '/mnt/user/' . $share . ($relative_path !== '' ? '/' . $relative_path : '');
|
||||
if (!is_dir($root) || !is_readable($root)) fail_status('Selected inventory root is not a readable directory');
|
||||
|
||||
$max_depth = max(1, min(20, $max_depth));
|
||||
$max_entries = max(1, min(5000, $max_entries));
|
||||
$needle = mb_strtolower(trim($name_contains));
|
||||
$queue = [[$root, '', 0, $needle === '']];
|
||||
$entries = [];
|
||||
$scanned = 0;
|
||||
$max_scanned = max(1000, min(100000, $max_entries * 50));
|
||||
$truncated = false;
|
||||
|
||||
while ($queue) {
|
||||
[$directory, $directory_relative, $depth, $inside_match] = array_shift($queue);
|
||||
if ($depth >= $max_depth) continue;
|
||||
$children = @scandir($directory);
|
||||
if (!is_array($children)) continue;
|
||||
natcasesort($children);
|
||||
foreach ($children as $name) {
|
||||
if ($name === '.' || $name === '..') continue;
|
||||
$full = $directory . '/' . $name;
|
||||
$relative = $directory_relative === '' ? $name : $directory_relative . '/' . $name;
|
||||
$is_link = is_link($full);
|
||||
$is_dir = !$is_link && is_dir($full);
|
||||
$matches = $needle === '' || mb_stripos($relative, $needle) !== false;
|
||||
$selected = $inside_match || $matches;
|
||||
$scanned++;
|
||||
if ($scanned > $max_scanned) {
|
||||
$truncated = true;
|
||||
break 2;
|
||||
}
|
||||
|
||||
if ($selected && (($is_dir && $include_directories) || (!$is_dir && $include_files))) {
|
||||
$extension = $is_dir ? '' : strtolower((string)pathinfo($name, PATHINFO_EXTENSION));
|
||||
$entries[] = [
|
||||
'relative_path' => $relative,
|
||||
'name' => $name,
|
||||
'type' => $is_link ? 'symlink' : ($is_dir ? 'directory' : 'file'),
|
||||
'extension' => $extension,
|
||||
'size_bytes' => (!$is_dir && !$is_link) ? max(0, (int)@filesize($full)) : 0,
|
||||
'depth' => $depth + 1,
|
||||
];
|
||||
if (count($entries) >= $max_entries) {
|
||||
$truncated = true;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
if ($is_dir) $queue[] = [$full, $relative, $depth + 1, $inside_match || $matches];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'schema_version' => '1.0',
|
||||
'share' => $share,
|
||||
'inventory_root' => $relative_path,
|
||||
'name_contains' => $name_contains,
|
||||
'entry_count' => count($entries),
|
||||
'scanned_entries' => $scanned,
|
||||
'truncated' => $truncated,
|
||||
'max_depth' => $max_depth,
|
||||
'max_entries' => $max_entries,
|
||||
'max_scanned_entries' => $max_scanned,
|
||||
'content_read' => false,
|
||||
'entries' => $entries,
|
||||
];
|
||||
}
|
||||
|
||||
function docker_update_state_value(mixed $value): ?string {
|
||||
if (is_array($value)) {
|
||||
foreach (['update_available', 'updateAvailable'] as $field) {
|
||||
@@ -356,6 +446,11 @@ if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
|
||||
case 'notifications': respond(notification_list((int)($argv[2] ?? 20), $argv[3] ?? 'all')); break;
|
||||
case 'shares-list': respond(shares_list(null)); break;
|
||||
case 'share-inspect': respond(shares_list($argv[2] ?? '')); break;
|
||||
case 'files-inventory': respond(files_inventory(
|
||||
$argv[2] ?? '', $argv[3] ?? '', $argv[4] ?? '',
|
||||
(int)($argv[5] ?? 10), (int)($argv[6] ?? 2000),
|
||||
($argv[7] ?? '1') !== '0', ($argv[8] ?? '1') !== '0'
|
||||
)); break;
|
||||
case 'docker-update-status': respond(docker_update_status()); break;
|
||||
default: fail_status('Unknown read-only status action');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user