Varaverk: arrange mode, folder management, rsync standalone, layout fixes
Scheduler UI: - Arrange mode: drag scripts between orchs and reorder within arrays; right panel shows unassigned script pool; Save Arrangement commits to master.conf - + Folder: named collapsible subfolders for Custom Scripts stored in schedule.json - Rsync children: hide Run/Dry Run/Log/location when orch is ON; show standalone location + cron controls when orch is OFF; cron only fires when both filled - Non-conf-managed children (transcode): toggles now show enabled when orch is on - Right panel height sync: fix ResizeObserver feedback loop via align-self:flex-start on left panel and left.offsetHeight in vvFitRight - How do I use this: updated to cover arrange, folders, rsync standalone, transcode New API endpoints: - board.php, clearlock.php, movescript.php, rawconf.php, readscript.php - reorderarray.php, rsync_standalone.php, savefolders.php run.php / dryrun.php: accept optional --location= arg for standalone rsync calls
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
// Live board data: locks, recent errors, partner reachability.
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
$out = ['ok' => true];
|
||||
|
||||
// ── Active locks ──────────────────────────────────────────────────────────
|
||||
$lockDir = '/tmp/unraid_locks';
|
||||
$locks = [];
|
||||
if (is_dir($lockDir)) {
|
||||
foreach (glob($lockDir . '/*.lock') ?: [] as $lf) {
|
||||
$name = basename($lf, '.lock');
|
||||
$age = time() - (int)filemtime($lf);
|
||||
$content = trim(file_get_contents($lf) ?: '');
|
||||
// content is "PID:scriptname" — extract PID
|
||||
$pid = preg_match('/^(\d+)/', $content, $pm) ? $pm[1] : '';
|
||||
// Skip if PID is still alive (it's legitimately running)
|
||||
if ($pid && file_exists("/proc/$pid")) continue;
|
||||
$locks[] = ['name' => $name, 'file' => basename($lf), 'age' => $age];
|
||||
}
|
||||
}
|
||||
$out['locks'] = $locks;
|
||||
|
||||
// ── Recent errors ──────────────────────────────────────────────────────────
|
||||
$errors = [];
|
||||
if (is_dir(LOG_DIR)) {
|
||||
$cutoff = time() - 7 * 86400; // only logs touched in last 7 days
|
||||
foreach (glob(LOG_DIR . '/*.log') ?: [] as $lf) {
|
||||
if (filemtime($lf) < $cutoff) continue;
|
||||
$script = basename($lf, '.log');
|
||||
$lines = array_slice(@file($lf) ?: [], -200);
|
||||
$lastErr = null;
|
||||
foreach (array_reverse($lines) as $raw) {
|
||||
// Strip ANSI escape codes
|
||||
$clean = preg_replace('/\033\[[0-9;]*[mK]/', '', rtrim($raw));
|
||||
if (!$clean) continue;
|
||||
if (preg_match('/\[(?:ERROR|WARN|CRITICAL|FAILED)\]/i', $clean) ||
|
||||
preg_match('/\b(?:ERROR|CRITICAL|FAILED):\s/i', $clean) ||
|
||||
str_contains($clean, '✗') ||
|
||||
(str_contains($clean, '⚠') && !str_contains($clean, '♥'))) {
|
||||
$lastErr = mb_substr($clean, 0, 220);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($lastErr !== null) {
|
||||
$errors[] = ['script' => $script, 'line' => $lastErr, 'ts' => (int)filemtime($lf)];
|
||||
}
|
||||
}
|
||||
usort($errors, fn($a, $b) => $b['ts'] - $a['ts']);
|
||||
}
|
||||
$out['errors'] = array_slice($errors, 0, 20);
|
||||
|
||||
// ── Partner reachability ───────────────────────────────────────────────────
|
||||
$cacheFile = '/tmp/vv_partner_cache.json';
|
||||
$cacheTtl = 30;
|
||||
$partnerData = null;
|
||||
|
||||
if (file_exists($cacheFile) && (time() - (int)filemtime($cacheFile)) < $cacheTtl) {
|
||||
$partnerData = json_decode(file_get_contents($cacheFile), true);
|
||||
} else {
|
||||
$confRaw = vv_read_conf_raw('master.conf');
|
||||
preg_match('/^\s*HOST1(?:_NAME)?\s*=\s*["\']?(\S+?)["\']?\s*(?:#.*)?$/m', $confRaw, $m1);
|
||||
preg_match('/^\s*HOST2(?:_NAME)?\s*=\s*["\']?(\S+?)["\']?\s*(?:#.*)?$/m', $confRaw, $m2);
|
||||
$host1 = $m1[1] ?? '';
|
||||
$host2 = $m2[1] ?? '';
|
||||
$mine = vv_get_hostname();
|
||||
$partnerHost = null;
|
||||
if ($mine && $host1 && strcasecmp($mine, $host1) === 0) $partnerHost = $host2;
|
||||
if ($mine && $host2 && strcasecmp($mine, $host2) === 0) $partnerHost = $host1;
|
||||
|
||||
if ($partnerHost) {
|
||||
$start = microtime(true);
|
||||
$result = shell_exec('ping -c1 -W1 ' . escapeshellarg($partnerHost) . ' 2>&1');
|
||||
$elapsed = (int)round((microtime(true) - $start) * 1000);
|
||||
$reached = str_contains((string)$result, '1 received')
|
||||
|| str_contains((string)$result, '1 packets received');
|
||||
$partnerData = [
|
||||
'host' => $partnerHost,
|
||||
'reachable' => $reached,
|
||||
'latency' => $reached ? $elapsed : null,
|
||||
];
|
||||
@file_put_contents($cacheFile, json_encode($partnerData));
|
||||
}
|
||||
}
|
||||
$out['partner'] = $partnerData;
|
||||
|
||||
echo json_encode($out);
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||
exit;
|
||||
}
|
||||
$file = basename($_POST['file'] ?? '');
|
||||
if (!$file || !preg_match('/^[a-zA-Z0-9_\-]+\.lock$/', $file)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid filename']);
|
||||
exit;
|
||||
}
|
||||
$path = '/tmp/unraid_locks/' . $file;
|
||||
if (file_exists($path)) @unlink($path);
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -21,7 +21,14 @@ if (!is_dir($logDir)) mkdir($logDir, 0755, true);
|
||||
|
||||
file_put_contents($logFile, "\n── " . date('Y-m-d H:i:s') . " [DRY RUN] ──────────────────────\n", FILE_APPEND);
|
||||
|
||||
$flags = vv_job_flags($id);
|
||||
exec('nohup env DRY_RUN=1 bash ' . escapeshellarg($script) . ($flags ? " $flags" : '') . ' >> ' . escapeshellarg($logFile) . ' 2>&1 </dev/null &');
|
||||
$location = trim($_POST['location'] ?? '');
|
||||
if ($location && (!str_starts_with($location, '/') || str_contains($location, '..') || preg_match('/[\x00\n\r]/', $location))) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid location']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$flags = vv_job_flags($id);
|
||||
$locArg = $location ? ' ' . escapeshellarg('--location=' . $location) : '';
|
||||
exec('nohup env DRY_RUN=1 bash ' . escapeshellarg($script) . ($flags ? " $flags" : '') . $locArg . ' >> ' . escapeshellarg($logFile) . ' 2>&1 </dev/null &');
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
// Move a script between *_SCRIPTS arrays in master.conf.
|
||||
// POST: script (rel path), to_array (var name, or '' to remove from all arrays).
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$script = trim($_POST['script'] ?? '');
|
||||
$toArray = trim($_POST['to_array'] ?? '');
|
||||
|
||||
if (!$script || str_contains($script, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $script)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid script']);
|
||||
exit;
|
||||
}
|
||||
if ($toArray && !preg_match('/^[A-Z_]+_SCRIPTS$/', $toArray)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid array name']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$confPath = CONF_DIR . '/master.conf';
|
||||
if (!file_exists($confPath)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'master.conf not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$lines = file($confPath, FILE_KEEP_BLANK_LINES);
|
||||
if (!$lines) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Could not read master.conf']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$scriptEsc = preg_quote($script, '/');
|
||||
$removedLine = null;
|
||||
$inArray = false;
|
||||
|
||||
// Step 1: find and remove the script line from whatever array it is currently in.
|
||||
$newLines = [];
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^\s*[A-Z_]+_SCRIPTS\s*=\s*\(/', $line)) $inArray = true;
|
||||
if ($inArray && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) $inArray = false;
|
||||
if ($inArray && preg_match('/^\s*(?:#\s*)?"' . $scriptEsc . '(?:\s[^"]*)?"/', $line)) {
|
||||
$removedLine = ' "' . $script . '"' . "\n"; // normalise indentation when re-inserting
|
||||
continue; // drop from current location
|
||||
}
|
||||
$newLines[] = $line;
|
||||
}
|
||||
|
||||
// Step 2: insert into target array (if specified).
|
||||
if ($toArray) {
|
||||
$resultLines = [];
|
||||
$inTarget = false;
|
||||
$inserted = false;
|
||||
foreach ($newLines as $line) {
|
||||
if (preg_match('/^\s*' . preg_quote($toArray, '/') . '\s*=\s*\(/', $line)) $inTarget = true;
|
||||
if ($inTarget && !$inserted && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) {
|
||||
$resultLines[] = $removedLine ?? (' "' . $script . '"' . "\n");
|
||||
$inTarget = false;
|
||||
$inserted = true;
|
||||
}
|
||||
$resultLines[] = $line;
|
||||
}
|
||||
if (!$inserted) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Target array "' . $toArray . '" not found in master.conf']);
|
||||
exit;
|
||||
}
|
||||
$newLines = $resultLines;
|
||||
}
|
||||
|
||||
if (file_put_contents($confPath, implode('', $newLines)) === false) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Write failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
// Raw conf read/write — respects per-host file visibility from vv_get_conf_files().
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
$allowed = vv_get_conf_files();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$file = trim($_GET['file'] ?? 'master.conf');
|
||||
if (!in_array($file, $allowed, true) || str_contains($file, '..')) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Not allowed']);
|
||||
exit;
|
||||
}
|
||||
echo json_encode(['ok' => true, 'content' => vv_read_conf_raw($file), 'file' => $file, 'allowed' => $allowed]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$file = trim($_POST['file'] ?? '');
|
||||
$content = $_POST['content'] ?? '';
|
||||
if (!in_array($file, $allowed, true) || str_contains($file, '..')) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Not allowed']);
|
||||
exit;
|
||||
}
|
||||
echo json_encode(['ok' => vv_write_conf_raw($file, $content)]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
// Read-only endpoint: return full content of any script in SCRIPTS_DIR.
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
$id = trim($_GET['id'] ?? '');
|
||||
|
||||
// Must be relative path within SCRIPTS_DIR, no traversal, must end in .sh or .md
|
||||
if (!$id || str_contains($id, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.(sh|md)$/', $id)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$path = SCRIPTS_DIR . '/' . $id;
|
||||
if (!file_exists($path)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true, 'content' => file_get_contents($path)]);
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
// Rewrite a *_SCRIPTS array in master.conf with a new script order.
|
||||
// POST: array_name (e.g. "DAILY_SCRIPTS"), scripts (JSON: [{"id":"rel/path.sh","enabled":true}, ...])
|
||||
// Preserves original entry lines (including inline flags/args) where possible.
|
||||
// Scripts absent from the new list are dropped; new scripts are added as fresh entries.
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$arrayName = trim($_POST['array_name'] ?? '');
|
||||
$raw = $_POST['scripts'] ?? '';
|
||||
$decoded = json_decode($raw, true);
|
||||
|
||||
if (!$arrayName || !preg_match('/^[A-Z_]+_SCRIPTS$/', $arrayName)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid array_name']);
|
||||
exit;
|
||||
}
|
||||
if (!is_array($decoded)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid scripts JSON']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate each entry
|
||||
$order = [];
|
||||
foreach ($decoded as $item) {
|
||||
$id = trim((string)($item['id'] ?? ''));
|
||||
$enabled = (bool)($item['enabled'] ?? true);
|
||||
if (!$id || str_contains($id, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $id)) continue;
|
||||
$order[] = ['id' => $id, 'enabled' => $enabled];
|
||||
}
|
||||
|
||||
$confPath = CONF_DIR . '/master.conf';
|
||||
if (!file_exists($confPath)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'master.conf not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$lines = file($confPath, FILE_KEEP_BLANK_LINES);
|
||||
if (!$lines) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Could not read master.conf']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Find the array block and extract original entry lines keyed by script path.
|
||||
$arrayEsc = preg_quote($arrayName, '/');
|
||||
$blockStart = null;
|
||||
$blockEnd = null;
|
||||
$depth = 0;
|
||||
$origEntries = []; // path → original trimmed content line (e.g. '"Daily/script.sh --flag"')
|
||||
|
||||
foreach ($lines as $i => $line) {
|
||||
if ($blockStart === null) {
|
||||
if (preg_match('/^\s*' . $arrayEsc . '\s*=\s*\(/', $line)) {
|
||||
$blockStart = $i;
|
||||
$depth = 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$depth += substr_count($line, '(');
|
||||
$depth -= substr_count($line, ')');
|
||||
if ($depth <= 0) {
|
||||
$blockEnd = $i;
|
||||
break;
|
||||
}
|
||||
// Collect entries (enabled and commented)
|
||||
if (preg_match('/^\s*(?:#\s*)?"([^"]+)"/', $line, $m)) {
|
||||
$parts = preg_split('/\s+/', trim($m[1]));
|
||||
$path = $parts[0] ?? '';
|
||||
if (substr($path, -3) === '.sh' && !isset($origEntries[$path])) {
|
||||
// Store the full quoted expression (may include flags after the path)
|
||||
$origEntries[$path] = '"' . $m[1] . '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($blockStart === null || $blockEnd === null) {
|
||||
echo json_encode(['ok' => false, 'error' => "Array $arrayName not found in master.conf"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Build replacement block lines
|
||||
$newBlockLines = [];
|
||||
// Preserve the opening line exactly (e.g. "DAILY_SCRIPTS=(")
|
||||
$newBlockLines[] = $lines[$blockStart];
|
||||
|
||||
foreach ($order as $item) {
|
||||
$id = $item['id'];
|
||||
$enabled = $item['enabled'];
|
||||
$entry = $origEntries[$id] ?? '"' . $id . '"';
|
||||
$prefix = $enabled ? ' ' : ' # ';
|
||||
$newBlockLines[] = $prefix . $entry . "\n";
|
||||
}
|
||||
|
||||
// Preserve the closing line exactly
|
||||
$newBlockLines[] = $lines[$blockEnd];
|
||||
|
||||
// Replace the original block in $lines
|
||||
array_splice($lines, $blockStart, $blockEnd - $blockStart + 1, $newBlockLines);
|
||||
|
||||
if (file_put_contents($confPath, implode('', $lines)) === false) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Write failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
// Save rsync standalone config (location + cron) for a specific rsync tier.
|
||||
// POST: flag_name (e.g. "DAILY_RSYNC_ENABLED"), orch_id, location, cron
|
||||
// Stored in schedule.json under "__rsync_{FLAG_NAME}".
|
||||
// Triggers a cron rebuild so the standalone entry takes effect immediately.
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$flagName = trim($_POST['flag_name'] ?? '');
|
||||
$orchId = trim($_POST['orch_id'] ?? '');
|
||||
$location = trim($_POST['location'] ?? '');
|
||||
$cron = trim($_POST['cron'] ?? '');
|
||||
|
||||
if (!$flagName || !preg_match('/^[A-Z_]+_RSYNC_ENABLED$/', $flagName)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid flag_name']);
|
||||
exit;
|
||||
}
|
||||
if ($orchId && (str_contains($orchId, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $orchId))) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid orch_id']);
|
||||
exit;
|
||||
}
|
||||
if ($location && (!str_starts_with($location, '/') || str_contains($location, '..') || preg_match('/[\x00\n\r]/', $location))) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid location']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$key = '__rsync_' . $flagName;
|
||||
$schedule = vv_schedule_load();
|
||||
$schedule[$key] = [
|
||||
'flag_name' => $flagName,
|
||||
'orch_id' => $orchId,
|
||||
'location' => $location,
|
||||
'cron' => $cron,
|
||||
];
|
||||
if (!vv_schedule_save($schedule)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Write failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
vv_cron_rebuild($schedule);
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -21,7 +21,14 @@ if (!is_dir($logDir)) mkdir($logDir, 0755, true);
|
||||
|
||||
file_put_contents($logFile, "\n── " . date('Y-m-d H:i:s') . " ──────────────────────\n", FILE_APPEND);
|
||||
|
||||
$flags = vv_job_flags($id);
|
||||
exec('nohup bash ' . escapeshellarg($script) . ($flags ? " $flags" : '') . ' >> ' . escapeshellarg($logFile) . ' 2>&1 </dev/null &');
|
||||
$location = trim($_POST['location'] ?? '');
|
||||
if ($location && (!str_starts_with($location, '/') || str_contains($location, '..') || preg_match('/[\x00\n\r]/', $location))) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid location']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$flags = vv_job_flags($id);
|
||||
$locArg = $location ? ' ' . escapeshellarg('--location=' . $location) : '';
|
||||
exec('nohup bash ' . escapeshellarg($script) . ($flags ? " $flags" : '') . $locArg . ' >> ' . escapeshellarg($logFile) . ' 2>&1 </dev/null &');
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
// Save custom-script folder assignments to schedule.json (__folders key).
|
||||
// POST: folders (JSON-encoded object: {"FolderName": ["Custom/script.sh", ...]})
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$raw = $_POST['folders'] ?? '';
|
||||
$decoded = json_decode($raw, true);
|
||||
if (!is_array($decoded)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid JSON']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$clean = [];
|
||||
foreach ($decoded as $name => $scripts) {
|
||||
$name = trim((string)$name);
|
||||
if (!$name || strlen($name) > 80) continue;
|
||||
if (!is_array($scripts)) continue;
|
||||
$cleanScripts = [];
|
||||
foreach ($scripts as $s) {
|
||||
$s = trim((string)$s);
|
||||
if (!$s || str_contains($s, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $s)) continue;
|
||||
$cleanScripts[] = $s;
|
||||
}
|
||||
$clean[$name] = $cleanScripts;
|
||||
}
|
||||
|
||||
$schedule = vv_schedule_load();
|
||||
$schedule['__folders'] = $clean;
|
||||
if (!vv_schedule_save($schedule)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Write failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
Reference in New Issue
Block a user