mirror of
https://github.com/michalcz10/USB-RAID-Array.git
synced 2026-11-11 07:59:04 +00:00
Redesign SFTP workspace and fix server monitoring
Replace Bootstrap dependencies with custom blue-accented light and dark themes and native dialogs. Share the sidebar, top bar and responsive layout across file browsing, account settings, user management, server statistics and file previews. Add folder search, type filters, upload progress and asset cache versioning. Fix SSH configuration usage, filesystem discovery, missing statistics and RAID health parsing. Adapt PDF sizing and fullscreen controls to the shared workspace. Validation: PHP workspace and server statistics regression tests, JavaScript syntax checks, responsive browser checks and live XAMPP navigation.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../Web/content/ftp/lib/serverstats.php';
|
||||
require_once __DIR__ . '/../Web/includes/workspace.php';
|
||||
|
||||
// Treat warnings and deprecations as failures, including when rendering missing data.
|
||||
set_error_handler(function ($severity, $message, $file, $line) {
|
||||
throw new ErrorException($message, 0, $severity, $file, $line);
|
||||
});
|
||||
function check($condition, $message) {
|
||||
if (!$condition) throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
$disk = parseDiskData("Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/mapper/vg-root 1048576 262144 786432 25% /\n");
|
||||
check($disk['available_data'] && $disk['size'] === '1 GiB' && $disk['usage'] === '25%', 'Parse POSIX df sizes and usage');
|
||||
foreach ([false, null, '', 'df: /dev/sdb1: No such file or directory', "Filesystem 1024-blocks Used Available Capacity Mounted on\n"] as $output) {
|
||||
check(!parseDiskData($output)['available_data'], 'Missing disk output must be unavailable');
|
||||
}
|
||||
$healthy = "Personalities : [raid1] [raid5]\nmd0 : active raid5 sda[0] sdb[1] sdc[2]\n 1000 blocks [3/3] [UUU]\n\nunused devices: <none>\n";
|
||||
check(parseRaidStatus($healthy)['status'] === 'healthy', 'Three-disk RAID5 should be healthy');
|
||||
check(parseRaidStatus(str_replace('[UUU]', '[U_U]', $healthy))['status'] === 'degraded', 'Missing RAID member should be degraded');
|
||||
check(parseRaidStatus($healthy . "md1 : active raid1 sdd[0] sde[1]\n 1000 blocks [2/1] [U_]\n")['status'] === 'degraded', 'One failed array must make overall status degraded');
|
||||
check(parseRaidStatus(str_replace('[UUU]', "[UUU]\n resync = 42.0%", $healthy))['status'] === 'syncing', 'RAID resync is visible');
|
||||
check(parseRaidStatus("md0 : active raid0 sda[0] sdb[1]\n 1000 blocks super 1.2 512k chunks\n")['status'] === 'active', 'RAID0 does not require a redundancy bitmap');
|
||||
check(parseRaidStatus("md0 : inactive sda[0](S)\n 1000 blocks\n")['status'] === 'inactive', 'Inactive array remains inactive');
|
||||
check(parseRaidStatus("Personalities : [raid1]\nunused devices: <none>\n")['status'] === 'not configured', 'No configured Linux MD array');
|
||||
check(parseRaidStatus(false)['status'] === 'unavailable', 'Unreadable mdstat is unavailable');
|
||||
$resources = parseSystemStats('97,5', '1024,256,768,0,0,0');
|
||||
check($resources['cpu']['usage'] === '2.5' && $resources['memory']['usage'] === '25%' && $resources['memory']['swap_usage'] === '0%', 'Resource values and zero swap');
|
||||
check(!parseSystemStats(false, false)['cpu']['available_data'], 'Missing CPU output must not show 100%');
|
||||
check(!parseSystemStats('not a number', '')['memory']['available_data'], 'Missing memory output must be unavailable');
|
||||
|
||||
// Check remote command construction and path quoting with a harmless stub.
|
||||
$ssh = new class {
|
||||
public $commands = [];
|
||||
function exec($command) { $this->commands[] = $command; return false; }
|
||||
};
|
||||
$defaultPath = "/storage/owner's files";
|
||||
$systemData = getSystemData($ssh, $defaultPath);
|
||||
check(in_array("LC_ALL=C df -Pk -- '/' 2>/dev/null", $ssh->commands, true), 'System filesystem uses root instead of a device name');
|
||||
check(in_array("LC_ALL=C df -Pk -- '/storage/owner'\\''s files' 2>/dev/null", $ssh->commands, true), 'Storage path is quoted for remote Linux even on Windows');
|
||||
|
||||
// Exercise the actual page template with missing output and with valid output.
|
||||
$source = file_get_contents(__DIR__ . '/../Web/content/ftp/serverstat.php');
|
||||
$template = substr($source, strpos($source, '$monitors ='));
|
||||
foreach ([false, true] as $available) {
|
||||
if ($available) {
|
||||
$systemData['storage'] = ['system' => $disk, 'data' => $disk];
|
||||
$systemData['resources'] = $resources;
|
||||
$systemData['raid'] = parseRaidStatus($healthy);
|
||||
}
|
||||
ob_start();
|
||||
eval($template);
|
||||
$html = ob_get_clean();
|
||||
check(strpos($html, 'Server status') !== false, 'Status page renders');
|
||||
check($available ? strpos($html, '1 GiB') !== false : strpos($html, 'Storage statistics are unavailable') !== false, 'Template handles available and missing data');
|
||||
}
|
||||
echo "Server statistics: parser, RAID, command quoting and page rendering checks passed.\n";
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../Web/includes/workspace.php';
|
||||
require_once __DIR__ . '/../Web/content/ftp/lib/serverstats.php';
|
||||
set_error_handler(function ($severity, $message, $file, $line) {
|
||||
if (error_reporting() & $severity) throw new ErrorException($message, 0, $severity, $file, $line);
|
||||
});
|
||||
function expectWorkspace($condition, $message) {
|
||||
if (!$condition) throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
function renderWorkspaceFixture($name, $admin = true) {
|
||||
$_SESSION = ['uname' => 'michal', 'admin' => $admin, 'upPer' => true, 'downPer' => true, 'delPer' => true];
|
||||
$defaultPath = '/mnt/raid'; $currentPath = '/mnt/raid/Workspace';
|
||||
$filePath = $currentPath . '/Example.pdf'; $fileName = basename($filePath); $parentDir = $currentPath;
|
||||
$fileExtension = 'png'; $fileSize = 245760; $mimeType = 'image/png';
|
||||
$isImage = true; $isVideo = false; $isAudio = false;
|
||||
$editable = true; $showLineNumbers = true; $content = "{\n \"workspace\": \"USB RAID\",\n \"status\": \"connected\"\n}\n";
|
||||
$message = ''; $messageType = ''; $message_type = ''; $shareCreated = false; $shareError = '';
|
||||
$share = ['expiration' => '2030-01-01']; $downloadAllowed = false; $token = 'test-only';
|
||||
$items = ['Documents', 'Photos', 'README.md', 'Example.pdf'];
|
||||
$imageExtensions = ['png']; $videoExtensions = []; $audioExtensions = []; $editableExtensions = ['md']; $archiveExtensions = [];
|
||||
$sftp = new class {
|
||||
function is_dir($name) { return strpos($name, '.') === false; }
|
||||
function stat($path) { return ['size' => 245760]; }
|
||||
};
|
||||
$result = new class {
|
||||
private $rows = [
|
||||
['uname' => 'michal', 'admin' => true, 'defPath' => '/mnt/raid', 'delPer' => true, 'downPer' => true, 'upPer' => true],
|
||||
['uname' => 'guest', 'admin' => false, 'defPath' => '/mnt/raid/shared', 'delPer' => false, 'downPer' => true, 'upPer' => false],
|
||||
];
|
||||
function fetch_assoc() { return array_shift($this->rows); }
|
||||
};
|
||||
$systemData = [
|
||||
'storage' => array_fill_keys(['system', 'data'], parseDiskData("Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/md0 1048576 262144 786432 25% /mnt/raid\n")),
|
||||
'raid' => parseRaidStatus("md0 : active raid5 sda[0] sdb[1] sdc[2]\n 1000 blocks [3/3] [UUU]\n"),
|
||||
'resources' => parseSystemStats('93', '8192,2048,6144,1024,0,1024'), 'lastUpdate' => '2026-10-16 10:00:00',
|
||||
];
|
||||
$top = in_array($name, ['adminpanel', 'changepassword'], true);
|
||||
$source = file_get_contents(__DIR__ . '/../Web/content/' . ($top ? '' : 'ftp/') . $name . '.php');
|
||||
if ($name === 'serverstat') {
|
||||
$start = strpos($source, '$monitors =');
|
||||
eval(substr($source, $start, strpos($source, '?>', $start) - $start));
|
||||
}
|
||||
$start = strpos($source, '<!DOCTYPE html>');
|
||||
$template = substr($source, $start, strpos($source, '</html>', $start) + 7 - $start);
|
||||
ob_start(); eval('?>' . $template); return ob_get_clean();
|
||||
}
|
||||
|
||||
$preview = in_array('--preview', $argv, true);
|
||||
$directory = __DIR__ . '/../Web/.layout-preview';
|
||||
if ($preview && !is_dir($directory)) mkdir($directory);
|
||||
$pages = ['index', 'serverstat', 'adminpanel', 'changepassword', 'open', 'view', 'pdf', 'shared'];
|
||||
foreach ($pages as $name) {
|
||||
$html = renderWorkspaceFixture($name);
|
||||
$dom = new DOMDocument();
|
||||
@$dom->loadHTML($html);
|
||||
$xpath = new DOMXPath($dom);
|
||||
expectWorkspace($xpath->query('//body')->length === 1, "$name has one body");
|
||||
expectWorkspace($xpath->query('//main')->length === 1, "$name has one main region");
|
||||
expectWorkspace($xpath->query('//aside[contains(@class,"sidebar")]')->length === 1, "$name has shared sidebar");
|
||||
expectWorkspace($xpath->query('//*[@id="themeToggle"]')->length === 1, "$name has one theme switch");
|
||||
expectWorkspace($xpath->query('//a[@aria-current="page"]')->length === 1, "$name has one active navigation item");
|
||||
expectWorkspace($xpath->query('//main//footer')->length === 1, "$name footer is inside the main region");
|
||||
if ($name === 'shared') expectWorkspace(strpos($html, 'User management') === false && strpos($html, 'Account settings') === false, 'Public shared preview does not expose account navigation');
|
||||
if ($preview) {
|
||||
$base = $name === 'adminpanel' || $name === 'changepassword' ? '../content/' : '../content/ftp/';
|
||||
$html = str_replace('<head>', '<head><base href="' . $base . '">', $html);
|
||||
// Browser previews never request a real remote file or perform mutations.
|
||||
$image = 'data:image/svg+xml,' . rawurlencode('<svg xmlns="http://www.w3.org/2000/svg" width="800" height="380"><rect width="800" height="380" fill="#1c3053"/><circle cx="400" cy="150" r="70" fill="#2563eb"/><text x="400" y="290" text-anchor="middle" fill="white" font-size="28" font-family="sans-serif">USB RAID / Workspace preview</text></svg>');
|
||||
$html = preg_replace('/src="(?:view|shared)\.php\?[^"\n]+"/', 'src="' . $image . '"', $html);
|
||||
$html = str_replace("const url = 'pdf.php?file=%2Fmnt%2Fraid%2FWorkspace%2FExample.pdf&stream=1';", "const url = '/Web/.layout-preview/sample.pdf';", $html);
|
||||
file_put_contents($directory . '/' . $name . '.html', $html);
|
||||
}
|
||||
}
|
||||
$member = renderWorkspaceFixture('index', false);
|
||||
expectWorkspace(strpos($member, 'User management') === false && strpos($member, '> Server status</a>') === false, 'Member navigation omits admin pages');
|
||||
expectWorkspace(strpos($member, 'Account settings') !== false, 'Members can access account settings');
|
||||
if ($preview) {
|
||||
// One-page PDF fixture for viewer navigation and fullscreen checks.
|
||||
$stream = "BT /F1 24 Tf 60 700 Td (USB RAID workspace preview) Tj ET";
|
||||
$objects = ['<< /Type /Catalog /Pages 2 0 R >>', '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>', '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>', '<< /Length ' . strlen($stream) . ">>\nstream\n" . $stream . "\nendstream"];
|
||||
$pdf = "%PDF-1.4\n"; $offsets = [0];
|
||||
foreach ($objects as $i => $object) { $offsets[] = strlen($pdf); $pdf .= ($i + 1) . " 0 obj\n$object\nendobj\n"; }
|
||||
$xref = strlen($pdf); $pdf .= "xref\n0 6\n0000000000 65535 f \n";
|
||||
foreach (array_slice($offsets, 1) as $offset) $pdf .= sprintf("%010d 00000 n \n", $offset);
|
||||
$pdf .= "trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n$xref\n%%EOF";
|
||||
file_put_contents($directory . '/sample.pdf', $pdf);
|
||||
}
|
||||
echo "Workspace: all eight page templates, navigation roles and shared layout checks passed.\n";
|
||||
Reference in New Issue
Block a user