diff --git a/build-plugin.sh b/build-plugin.sh index 5b519fa..4c2c627 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.06.23.r013" +VERSION="2026.06.23.r014" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.06.23.r013.tgz b/packages/u-navigator-2026.06.23.r013.tgz deleted file mode 100644 index 070a3cb..0000000 Binary files a/packages/u-navigator-2026.06.23.r013.tgz and /dev/null differ diff --git a/packages/u-navigator-2026.06.23.r014.tgz b/packages/u-navigator-2026.06.23.r014.tgz new file mode 100644 index 0000000..3bebf93 Binary files /dev/null and b/packages/u-navigator-2026.06.23.r014.tgz differ 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 cd750af..062989b 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 @@ -5,17 +5,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { unav_json(unav_job_read($_GET['id'] ?? '')); } -unav_assert_writable(); - $action = $_GET['action'] ?? ''; -if (!in_array($action, ['move', 'copy'], true)) { +if (!in_array($action, ['move', 'copy', 'size'], true)) { unav_error(404, 'Unknown job action'); } $data = unav_request_data(); $source = unav_existing_path($data['source'] ?? null); -$destination = unav_destination_path($data['destination'] ?? null); -unav_guard_destination($source['path'], $destination['path']); +$destination = 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']); +} $id = str_replace('.', '', uniqid($action . '-', true)); $now = date(DATE_ATOM); @@ -27,7 +29,7 @@ $job = [ 'message' => 'Queued', 'error' => null, 'source' => $source['path'], - 'destination' => $destination['path'], + 'destination' => $destination['path'] ?? null, '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 cdda58f..0396fe5 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 @@ -9,12 +9,16 @@ function unav_run_job(string $id): void { $job = unav_job_read($id); try { unav_worker_update($job, 'running', 5, 'Preparing'); - unav_worker_guard($job['source'], $job['destination']); if ($job['type'] === 'move') { + unav_worker_guard($job['source'], $job['destination']); unav_worker_move($job['source'], $job['destination'], $job); } elseif ($job['type'] === 'copy') { + unav_worker_guard($job['source'], $job['destination']); unav_worker_copy($job['source'], $job['destination'], $job); + } elseif ($job['type'] === 'size') { + $result = unav_worker_size($job['source'], $job); + $job['result'] = $result; } else { throw new RuntimeException('Unknown job action'); } @@ -55,6 +59,15 @@ function unav_worker_guard(string $source, string $destination): void { } } +function unav_worker_guard_source(string $source): string { + $sourceReal = realpath($source); + if ($sourceReal === false) { + throw new RuntimeException('Source not found'); + } + unav_worker_assert_inside_root($sourceReal); + return $sourceReal; +} + function unav_worker_assert_inside_root(string $path): void { foreach (unav_roots() as $root) { if (unav_is_inside($path, $root['path'])) { @@ -73,6 +86,38 @@ function unav_worker_move(string $source, string $destination, array &$job): voi unav_worker_delete($source); } +function unav_worker_size(string $source, array &$job): array { + $sourceReal = unav_worker_guard_source($source); + unav_worker_update($job, 'running', 10, 'Calculating size'); + $result = unav_worker_size_recursive($sourceReal); + unav_worker_update($job, 'running', 95, 'Size calculated'); + return $result; +} + +function unav_worker_size_recursive(string $path): array { + if (is_link($path)) { + return ['size' => 0, 'files' => 0, 'directories' => 0]; + } + if (is_file($path)) { + return ['size' => filesize($path) ?: 0, 'files' => 1, 'directories' => 0]; + } + if (!is_dir($path)) { + return ['size' => 0, 'files' => 0, 'directories' => 0]; + } + + $total = ['size' => 0, 'files' => 0, 'directories' => 1]; + foreach (scandir($path) ?: [] as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + $child = unav_worker_size_recursive($path . '/' . $entry); + $total['size'] += $child['size']; + $total['files'] += $child['files']; + $total['directories'] += $child['directories']; + } + return $total; +} + 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 6b8edad..6174e80 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -15,6 +15,8 @@ const state = { nextDebugId: 1, jobs: new Map(), jobRefresh: new Map(), + sizeJobs: new Map(), + sizeResults: new Map(), nextWindowId: 1, zIndex: 20 }; @@ -339,19 +341,23 @@ function renderProperties(body) { return; } + const sizeLabel = entry.type === 'directory' ? directorySizeLabel(entry) : formatBytes(entry.size); body.classList.add('properties'); body.innerHTML = `
Name
${escapeHtml(entry.name)}
Typ
${escapeHtml(entry.type)}
Pfad
${escapeHtml(entry.path)}
-
Größe
${formatBytes(entry.size)}
+
Größe
${escapeHtml(sizeLabel)}
Geändert
${new Date(entry.modifiedAt).toLocaleString()}
Besitzer
${escapeHtml(formatPrincipal(entry.owner, entry.ownerId))}
Gruppe
${escapeHtml(formatPrincipal(entry.group, entry.groupId))}
Rechte
${escapeHtml(entry.permissions ? `0${entry.permissions}`.slice(-4) : '-')}
`; + if (entry.type === 'directory') { + ensureDirectorySize(entry); + } } function renderQueue(body) { @@ -452,6 +458,24 @@ async function createJob(type, source, destination, refreshWindowIds = []) { return job; } +async function createSizeJob(entry) { + recordDebug('size.create.request', { source: entry.path }); + const body = new URLSearchParams(); + body.set('source', entry.path); + appendCsrf(body); + const job = await api(apiUrl('job.php?action=size'), { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, + body + }); + recordDebug('size.create.response', job); + state.jobs.set(job.id, job); + state.sizeJobs.set(entry.path, job.id); + pollJob(job.id); + render(); + return job; +} + async function reloadExplorerWindows(ids) { const uniqueIds = new Set(ids); const targets = state.windows.filter((win) => win.kind === 'explorer' && (uniqueIds.has(win.id) || !uniqueIds.size)); @@ -601,6 +625,8 @@ async function pollJob(id) { render(); if (['queued', 'running', 'cancel_requested'].includes(job.status)) { setTimeout(() => pollJob(id), 700); + } else if (job.type === 'size') { + finishSizeJob(job); } else { await refreshAfterJob(job); } @@ -610,6 +636,26 @@ async function pollJob(id) { } } +function ensureDirectorySize(entry) { + if (state.sizeResults.has(entry.path) || state.sizeJobs.has(entry.path)) { + return; + } + state.sizeJobs.set(entry.path, 'pending'); + createSizeJob(entry).catch((error) => { + state.sizeJobs.delete(entry.path); + showError(error); + }); +} + +function finishSizeJob(job) { + state.sizeJobs.delete(job.source); + if (job.status === 'completed' && job.result) { + state.sizeResults.set(job.source, job.result); + } + recordDebug('size.finish', { id: job.id, source: job.source, status: job.status, result: job.result || null }); + updatePropertiesWindows(); +} + async function refreshAfterJob(job) { const refresh = state.jobRefresh.get(job.id); if (refresh?.done) { @@ -961,6 +1007,16 @@ function formatBytes(bytes) { return `${(value / 1024 / 1024 / 1024).toFixed(1)} GB`; } +function directorySizeLabel(entry) { + const result = state.sizeResults.get(entry.path); + if (!result) { + return state.sizeJobs.has(entry.path) ? 'Berechne...' : 'Noch nicht berechnet'; + } + const files = Number(result.files || 0); + const directories = Math.max(0, Number(result.directories || 0) - 1); + return `${formatBytes(result.size)} · ${files} Dateien · ${directories} Ordner`; +} + function formatPrincipal(name, id) { if (name == null && id == null) return '-'; if (name == null || String(name) === String(id)) return String(id); diff --git a/u-navigator.plg b/u-navigator.plg index 537d880..c93f6c9 100644 --- a/u-navigator.plg +++ b/u-navigator.plg @@ -1,7 +1,7 @@ - + ]> @@ -14,7 +14,7 @@ https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package; -b72f22b37e34b4bdfd93a57c138af2d0 +0100a8a8507d664658a9451a40881944