diff --git a/build-plugin.sh b/build-plugin.sh index 783970d..fe9af14 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.06.23.r037" +VERSION="2026.06.23.r038" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.06.23.r037.tgz b/packages/u-navigator-2026.06.23.r037.tgz deleted file mode 100644 index 07f3ed5..0000000 Binary files a/packages/u-navigator-2026.06.23.r037.tgz and /dev/null differ diff --git a/packages/u-navigator-2026.06.23.r038.tgz b/packages/u-navigator-2026.06.23.r038.tgz new file mode 100644 index 0000000..c5affce Binary files /dev/null and b/packages/u-navigator-2026.06.23.r038.tgz differ diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php index 7661240..ea1fde8 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php @@ -211,6 +211,33 @@ function unav_guard_destination(string $source, string $destination): void { } } +function unav_permission_options(array $data): array { + $mode = trim((string)($data['mode'] ?? '')); + $owner = trim((string)($data['owner'] ?? '')); + $group = trim((string)($data['group'] ?? '')); + $recursive = filter_var($data['recursive'] ?? false, FILTER_VALIDATE_BOOLEAN); + + if ($mode === '' && $owner === '' && $group === '') { + unav_error(400, 'No permission changes requested'); + } + if ($mode !== '' && !preg_match('/^[0-7]{3,4}$/', $mode)) { + unav_error(400, 'Invalid permission mode'); + } + if ($owner !== '' && !preg_match('/^[A-Za-z0-9._-]+$/', $owner)) { + unav_error(400, 'Invalid owner'); + } + if ($group !== '' && !preg_match('/^[A-Za-z0-9._-]+$/', $group)) { + unav_error(400, 'Invalid group'); + } + + return [ + 'mode' => $mode === '' ? null : $mode, + 'owner' => $owner === '' ? null : $owner, + 'group' => $group === '' ? null : $group, + 'recursive' => $recursive, + ]; +} + function unav_job_dir(): string { if (!is_dir(UNAV_JOB_DIR) && !mkdir(UNAV_JOB_DIR, 0777, true) && !is_dir(UNAV_JOB_DIR)) { unav_error(500, 'Could not create job directory'); diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php index 062989b..685cf9b 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php @@ -6,17 +6,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { } $action = $_GET['action'] ?? ''; -if (!in_array($action, ['move', 'copy', 'size'], true)) { +if (!in_array($action, ['move', 'copy', 'size', 'permissions'], true)) { unav_error(404, 'Unknown job action'); } $data = unav_request_data(); $source = unav_existing_path($data['source'] ?? null); $destination = null; +$permissions = null; if (in_array($action, ['move', 'copy'], true)) { unav_assert_writable(); $destination = unav_destination_path($data['destination'] ?? null); unav_guard_destination($source['path'], $destination['path']); +} elseif ($action === 'permissions') { + unav_assert_writable(); + $permissions = unav_permission_options($data); } $id = str_replace('.', '', uniqid($action . '-', true)); @@ -30,6 +34,7 @@ $job = [ 'error' => null, 'source' => $source['path'], 'destination' => $destination['path'] ?? null, + 'permissions' => $permissions, 'createdAt' => $now, 'updatedAt' => $now, ]; diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job_worker.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job_worker.php index 04b364e..59cd125 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job_worker.php +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job_worker.php @@ -19,6 +19,8 @@ function unav_run_job(string $id): void { } elseif ($job['type'] === 'size') { $result = unav_worker_size($job['source'], $job); $job['result'] = $result; + } elseif ($job['type'] === 'permissions') { + unav_worker_permissions($job['source'], $job['permissions'] ?? [], $job); } else { throw new RuntimeException('Unknown job action'); } @@ -120,6 +122,57 @@ function unav_worker_size_recursive(string $path): array { return $total; } +function unav_worker_permissions(string $source, array $options, array &$job): void { + $sourceReal = unav_worker_guard_source($source); + $mode = $options['mode'] ?? null; + $owner = $options['owner'] ?? null; + $group = $options['group'] ?? null; + $recursive = filter_var($options['recursive'] ?? false, FILTER_VALIDATE_BOOLEAN); + + if ($mode === null && $owner === null && $group === null) { + throw new RuntimeException('No permission changes requested'); + } + + unav_worker_update($job, 'running', 10, 'Updating permissions'); + $count = 0; + unav_worker_permissions_apply($sourceReal, $mode, $owner, $group, $recursive, $job, $count); + $job['result'] = ['changed' => $count]; + unav_worker_update($job, 'running', 95, 'Permissions updated'); +} + +function unav_worker_permissions_apply(string $path, ?string $mode, ?string $owner, ?string $group, bool $recursive, array &$job, int &$count): void { + if (is_link($path)) { + return; + } + if (!file_exists($path)) { + throw new RuntimeException('Path not found'); + } + + if ($mode !== null && !chmod($path, intval($mode, 8))) { + throw new RuntimeException('Could not change permissions'); + } + if ($owner !== null && !chown($path, $owner)) { + throw new RuntimeException('Could not change owner'); + } + if ($group !== null && !chgrp($path, $group)) { + throw new RuntimeException('Could not change group'); + } + $count++; + if ($count % 100 === 0) { + unav_worker_update($job, 'running', 50, 'Updating permissions'); + } + + if (!$recursive || !is_dir($path)) { + return; + } + foreach (scandir($path) ?: [] as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + unav_worker_permissions_apply($path . '/' . $entry, $mode, $owner, $group, true, $job, $count); + } +} + function unav_worker_copy(string $source, string $destination, array &$job): void { if (is_link($source)) { throw new RuntimeException('Copying symlinks is not allowed'); diff --git a/server/public/app.js b/server/public/app.js index ffda5de..7f8d51d 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -343,7 +343,13 @@ function renderExplorer(body, win) { row.addEventListener('contextmenu', (event) => { event.preventDefault(); event.stopPropagation(); - selectEntry(win, entry, null, event); + if (selectedPathSet(win).has(entry.path)) { + state.selectedEntry = entry; + focusWindow(win); + updatePropertiesWindows(); + } else { + selectEntry(win, entry, null, event); + } showContextMenu(event, win, entry); }); } @@ -875,15 +881,22 @@ async function handleDrop(event, win, targetPath) { } } -async function createJob(type, source, destination, refreshWindowIds = []) { - if (['move', 'copy'].includes(type) && isReadOnly()) { +async function createJob(type, source, destination = null, refreshWindowIds = [], extra = {}) { + if (['move', 'copy', 'permissions'].includes(type) && isReadOnly()) { showError(new Error('Read-only mode ist aktiv.')); return null; } - recordDebug('job.create.request', { type, source, destination, refreshWindowIds }); + recordDebug('job.create.request', { type, source, destination, refreshWindowIds, extra }); const body = new URLSearchParams(); body.set('source', source); - body.set('destination', destination); + if (destination !== null) { + body.set('destination', destination); + } + for (const [key, value] of Object.entries(extra)) { + if (value !== null && value !== undefined) { + body.set(key, String(value)); + } + } appendCsrf(body); const job = await api(apiUrl(`job.php?action=${encodeURIComponent(type)}`), { method: 'POST', @@ -999,6 +1012,35 @@ async function promptTransfer(win, entry, type) { await createJob(type, entry.path, `${targetDir.replace(/\/$/, '')}/${entry.name}`, []); } +async function promptPermissions(win, entry) { + if (isReadOnly()) { + showError(new Error('Read-only mode ist aktiv.')); + return; + } + const entries = selectedPathSet(win).has(entry.path) ? selectedEntries(win) : [entry]; + const mode = prompt('Berechtigungen als Oktalmodus, z.B. 0644 oder 0755', entry.permissions ? `0${entry.permissions}`.slice(-4) : ''); + if (mode === null) return; + const owner = prompt('Besitzer ändern, leer lassen für unverändert', entry.owner || ''); + if (owner === null) return; + const group = prompt('Gruppe ändern, leer lassen für unverändert', entry.group || ''); + if (group === null) return; + const hasDirectory = entries.some((item) => item.type === 'directory'); + const recursive = hasDirectory && confirm('Rekursiv auf Unterordner und Dateien anwenden?'); + const options = { + mode: mode.trim(), + owner: owner.trim(), + group: group.trim(), + recursive: recursive ? '1' : '0' + }; + if (!options.mode && !options.owner && !options.group) { + showError(new Error('Keine Änderung angegeben.')); + return; + } + for (const item of entries) { + await createJob('permissions', item.path, null, [win.id], options); + } +} + function showPropertiesForEntry(win, entry) { selectEntry(win, entry); openProperties(entry); @@ -1034,6 +1076,7 @@ function renderContextMenu() { actions.push(['Eigenschaften', () => showPropertiesForEntry(win, entry)]); if (!isReadOnly()) { actions.push(['Umbenennen', () => renameEntry(win, entry)]); + actions.push(['Berechtigungen...', () => promptPermissions(win, entry)]); actions.push(['Kopieren nach...', () => promptTransfer(win, entry, 'copy')]); actions.push(['Verschieben nach...', () => promptTransfer(win, entry, 'move')]); } diff --git a/u-navigator.plg b/u-navigator.plg index 9a1f749..5781309 100644 --- a/u-navigator.plg +++ b/u-navigator.plg @@ -1,7 +1,7 @@ - + @@ -24,7 +24,7 @@ https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package; -6ae4016e68bf99a8cb0c64714b8cb647 +87661843082c0f41bd627a315278e346