feat: add bounded file inventory for media audits

This commit is contained in:
Mikei386
2026-08-24 11:49:12 +02:00
parent d90874ed18
commit 4426969d63
12 changed files with 163 additions and 12 deletions
+95
View File
@@ -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');
}