Calculate directory sizes in background

This commit is contained in:
Mikei386
2026-06-23 09:17:32 +02:00
parent 74ba8d0984
commit 53e25c4641
7 changed files with 114 additions and 11 deletions
+1 -1
View File
@@ -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"
Binary file not shown.
Binary file not shown.
@@ -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 = 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,
];
@@ -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');
+57 -1
View File
@@ -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 = `
<dl>
<dt>Name</dt><dd>${escapeHtml(entry.name)}</dd>
<dt>Typ</dt><dd>${escapeHtml(entry.type)}</dd>
<dt>Pfad</dt><dd>${escapeHtml(entry.path)}</dd>
<dt>Größe</dt><dd>${formatBytes(entry.size)}</dd>
<dt>Größe</dt><dd>${escapeHtml(sizeLabel)}</dd>
<dt>Geändert</dt><dd>${new Date(entry.modifiedAt).toLocaleString()}</dd>
<dt>Besitzer</dt><dd>${escapeHtml(formatPrincipal(entry.owner, entry.ownerId))}</dd>
<dt>Gruppe</dt><dd>${escapeHtml(formatPrincipal(entry.group, entry.groupId))}</dd>
<dt>Rechte</dt><dd>${escapeHtml(entry.permissions ? `0${entry.permissions}`.slice(-4) : '-')}</dd>
</dl>
`;
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);
+2 -2
View File
@@ -1,7 +1,7 @@
<!DOCTYPE PLUGIN [
<!ENTITY name "u-navigator">
<!ENTITY author "michael">
<!ENTITY version "2026.06.23.r013">
<!ENTITY version "2026.06.23.r014">
<!ENTITY package "&name;-&version;.tgz">
]>
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/u-navigator.plg">
@@ -14,7 +14,7 @@
<FILE Name="/boot/config/plugins/&name;/&package;">
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
<MD5>b72f22b37e34b4bdfd93a57c138af2d0</MD5>
<MD5>0100a8a8507d664658a9451a40881944</MD5>
</FILE>
<FILE Run="/bin/bash">