diff --git a/Manual.md b/Manual.md index ed5d231..fa2d509 100644 --- a/Manual.md +++ b/Manual.md @@ -305,7 +305,31 @@ entries needed. The plugin handles all triggers natively: - **Cron** → `Plugin/unraid/event/disks_mounted/rebuild_cron` rebuilds the cron file from `schedule.json` on every boot Configure via the Varaverk plugin Scheduler tab (or edit `schedule.json` directly). -Individual scripts are never scheduled — only orchestrators. +The built-in job list schedules orchestrators, never individual repo scripts directly — +but the Scheduler tab's **Custom Scripts** card is the one place individual scripts +*are* scheduled directly (see below). + +--- + +### ── Custom Scripts ──────────────────────────────────────────────────────────── + +The Scheduler tab has a **Custom Scripts** card for one-off scripts that aren't part of +the repo's orchestrator pipeline — personal tooling, quick fixes, anything you don't +want to wire into `master.conf`. + +Scripts live in `/boot/config/plugins/user.scripts/Varaverk/Scripts/` — deliberately +**outside** the Varaverk git repo (that folder is never pushed to GitHub), in the same +place the Unraid User Scripts plugin keeps its own scripts, so it's a folder location +admins are already used to. + +Two ways to get a script there: + +- Click **+ Create Script** on the Scheduler tab — opens an inline editor, writes the + file to that folder, and adds a `schedule.json` entry automatically. +- Drop any `.sh` file into the folder yourself (e.g. via terminal, or Unraid's own + Custom Scripts / User Scripts plugin pointed at the same path). The Scheduler tab + **auto-detects** it — discovery is a folder scan, not a registry, so it doesn't matter + how the file got there. It shows up disabled with no cron until you configure one. --- diff --git a/Plugin/Manual-Plugin.md b/Plugin/Manual-Plugin.md index 4c58e98..33dcf79 100644 --- a/Plugin/Manual-Plugin.md +++ b/Plugin/Manual-Plugin.md @@ -73,8 +73,8 @@ to Community Applications. ## ━━━ SCRIPTS DIRECTORY SETTING ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -`SCRIPTS_DIR` is the only plugin-level setting. It tells the plugin where to find the -Configurations directory and all scripts. +`SCRIPTS_DIR` tells the plugin where to find the Configurations directory and all +repo scripts. **Set it via:** Settings → Other Settings → Varaverk → Scripts directory @@ -88,6 +88,18 @@ SCRIPTS_DIR="/boot/config/plugins/varaverk" All other configuration lives in `Configurations/master.conf` and `Configurations/host*.conf`. +`CUSTOM_SCRIPTS_DIR` is a separate, optional override for where the Scheduler tab's +Custom Scripts feature reads/writes user-authored scripts (see Manual.md → Custom +Scripts). It's intentionally **not** under `SCRIPTS_DIR` — Custom Scripts are personal, +non-repo tooling and must never end up inside the git-tracked plugin folder. + +Default: `/boot/config/plugins/user.scripts/Varaverk/Scripts` + +```bash +# in varaverk.cfg, alongside SCRIPTS_DIR +CUSTOM_SCRIPTS_DIR="/boot/config/plugins/user.scripts/Varaverk/Scripts" +``` + --- ## ━━━ REPO MOVE PROCEDURE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/Plugin/unraid/api/import_script.php b/Plugin/unraid/api/import_script.php new file mode 100644 index 0000000..1abf97f --- /dev/null +++ b/Plugin/unraid/api/import_script.php @@ -0,0 +1,105 @@ + false, 'error' => 'Invalid path']); + exit; + } + $clean = rtrim($path, '/') ?: '/'; + if (!is_dir($clean)) { + echo json_encode(['ok' => false, 'error' => 'Not a directory: ' . $clean]); + exit; + } + + $dirOut = shell_exec('find ' . escapeshellarg($clean) . ' -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sort | head -300') ?: ''; + $dirs = array_values(array_filter(array_map('trim', explode("\n", $dirOut)))); + + $fileOut = shell_exec('find ' . escapeshellarg($clean) . ' -maxdepth 1 -mindepth 1 -type f -iname "*.sh" 2>/dev/null | sort | head -300') ?: ''; + $files = array_values(array_filter(array_map('trim', explode("\n", $fileOut)))); + + $parent = ($clean !== '/') ? (dirname($clean) ?: '/') : null; + echo json_encode(['ok' => true, 'path' => $clean, 'dirs' => $dirs, 'files' => $files, 'parent' => $parent]); + exit; +} + +// ── import (POST): move the chosen .sh file into CUSTOM_SCRIPTS_DIR ─────────── +if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'import') { + $src = trim($_POST['path'] ?? ''); + if (!preg_match('#^/[^\0]*\.sh$#i', $src) || str_contains($src, '..')) { + echo json_encode(['ok' => false, 'error' => 'Invalid script path']); + exit; + } + if (!is_file($src)) { + echo json_encode(['ok' => false, 'error' => 'Not found: ' . $src]); + exit; + } + + $srcReal = realpath($src); + if ($srcReal === false) { + echo json_encode(['ok' => false, 'error' => 'Could not resolve path']); + exit; + } + + // Refuse to move a file out of the git-tracked repo — that would delete a + // tracked file out from under git without a commit recording it. + $repoReal = realpath(SCRIPTS_DIR); + if ($repoReal && str_starts_with($srcReal, $repoReal . '/')) { + echo json_encode(['ok' => false, 'error' => 'Refusing to import from inside the Varaverk repo — that would delete a git-tracked file.']); + exit; + } + + // Already there — nothing to do. + $customReal = realpath(CUSTOM_SCRIPTS_DIR) ?: CUSTOM_SCRIPTS_DIR; + if (str_starts_with($srcReal, rtrim($customReal, '/') . '/')) { + echo json_encode(['ok' => false, 'error' => 'Already in Custom Scripts.']); + exit; + } + + if (!is_dir(CUSTOM_SCRIPTS_DIR)) mkdir(CUSTOM_SCRIPTS_DIR, 0755, true); + + $name = basename($srcReal); + $dest = CUSTOM_SCRIPTS_DIR . '/' . $name; + if (file_exists($dest)) { + echo json_encode(['ok' => false, 'error' => "A script named \"$name\" already exists in Custom Scripts."]); + exit; + } + + // Copy across filesystems, verify, THEN delete the source — never remove the + // only copy on a failed or partial copy. + if (!copy($srcReal, $dest)) { + @unlink($dest); + echo json_encode(['ok' => false, 'error' => 'Copy failed']); + exit; + } + if (filesize($srcReal) !== filesize($dest) || hash_file('sha256', $srcReal) !== hash_file('sha256', $dest)) { + @unlink($dest); + echo json_encode(['ok' => false, 'error' => 'Copy verification failed — source left untouched']); + exit; + } + chmod($dest, 0755); + + if (!@unlink($srcReal)) { + // Copied and verified but couldn't remove the original (permissions, read-only + // mount). The script is usable from its new home either way — surface a warning + // rather than failing the import outright. + echo json_encode([ + 'ok' => true, + 'id' => 'Custom/' . $name, + 'warning' => 'Imported, but could not delete the original at ' . $srcReal . ' — remove it manually.', + ]); + exit; + } + + echo json_encode(['ok' => true, 'id' => 'Custom/' . $name]); + exit; +} + +echo json_encode(['ok' => false, 'error' => 'Invalid request']); diff --git a/Plugin/unraid/api/script.php b/Plugin/unraid/api/script.php index 2a9e32f..5386cb0 100644 --- a/Plugin/unraid/api/script.php +++ b/Plugin/unraid/api/script.php @@ -8,7 +8,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { echo json_encode(['ok' => false, 'error' => 'Invalid id']); exit; } - $path = SCRIPTS_DIR . '/' . $id; + $path = CUSTOM_SCRIPTS_DIR . '/' . substr($id, strlen('Custom/')); echo json_encode(['ok' => true, 'content' => file_exists($path) ? file_get_contents($path) : '']); exit; } @@ -24,7 +24,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } $id = 'Custom/' . $name . '.sh'; - $path = SCRIPTS_DIR . '/Custom/' . $name . '.sh'; + $path = CUSTOM_SCRIPTS_DIR . '/' . $name . '.sh'; if ($action === 'delete') { if (!file_exists($path)) { @@ -40,7 +40,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { exit; } - $dir = SCRIPTS_DIR . '/Custom'; + $dir = CUSTOM_SCRIPTS_DIR; if (!is_dir($dir)) mkdir($dir, 0755, true); if (file_put_contents($path, $content) === false) { echo json_encode(['ok' => false, 'error' => 'Failed to write script']); diff --git a/Plugin/unraid/include/config.php b/Plugin/unraid/include/config.php index 7d76126..93a35f9 100644 --- a/Plugin/unraid/include/config.php +++ b/Plugin/unraid/include/config.php @@ -11,6 +11,11 @@ define('DEPLOY_DIR', SCRIPTS_DIR . '/Deployment'); define('DATA_DIR', SCRIPTS_DIR . '/data'); define('STATE_DIR', SCRIPTS_DIR . '/State_Files'); define('LOG_DIR', '/var/log/varaverk'); +// User-authored custom scripts (scheduler page "+ Create Script") — kept outside the git +// repo entirely, alongside the User Scripts plugin's own storage. Any *.sh file placed +// directly in this folder is auto-detected and listed — it doesn't have to be created +// through the page's editor. +define('CUSTOM_SCRIPTS_DIR', $_vv_cfg['CUSTOM_SCRIPTS_DIR'] ?? '/boot/config/plugins/user.scripts/Varaverk/Scripts'); unset($_vv_cfg); define('VV_SETUP_STATE_FILE', STATE_DIR . '/varaverk_setup.db'); diff --git a/Plugin/unraid/include/scheduler.php b/Plugin/unraid/include/scheduler.php index d93c45e..8ae4cd2 100644 --- a/Plugin/unraid/include/scheduler.php +++ b/Plugin/unraid/include/scheduler.php @@ -97,7 +97,10 @@ function vv_cron_rebuild(array $schedule): bool { $id = $entry['id']; // When a child's orch is enabled it is the sole trigger — suppress independent cron. if (isset($childToOrch[$id]) && !empty($schedule[$childToOrch[$id]]['enabled'])) continue; - $script = "$scriptsDir/$id"; + // Custom Scripts live outside the repo (CUSTOM_SCRIPTS_DIR) — everything else resolves under SCRIPTS_DIR. + $script = str_starts_with($id, 'Custom/') + ? CUSTOM_SCRIPTS_DIR . '/' . substr($id, strlen('Custom/')) + : "$scriptsDir/$id"; $flags = !empty($entry['log_enabled']) ? ' --log' : ''; $lines[] = "{$entry['cron']} bash \"$runner\" \"$id\" \"$script\"$flags"; } @@ -278,6 +281,10 @@ function vv_tools_scripts(): array { return $scripts; } +// Lists every Custom Script for the scheduler page. Discovery is glob-based, not a +// registry — any *.sh file dropped directly into CUSTOM_SCRIPTS_DIR (or a platform +// adapter's own Custom/ folder) shows up here, whether or not it was created via the +// page's "+ Create Script" editor or has a schedule.json entry yet. function vv_custom_scripts(): array { $schedule = vv_schedule_load(); $scripts = []; @@ -297,7 +304,7 @@ function vv_custom_scripts(): array { } }; - $collect(SCRIPTS_DIR . '/Custom', 'Custom/'); + $collect(CUSTOM_SCRIPTS_DIR, 'Custom/'); // Platform adapter custom scripts (Plugin//Custom/) foreach (glob(SCRIPTS_DIR . '/Plugin/*/Custom') ?: [] as $customDir) { diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php index 01f49c8..e3ffd91 100644 --- a/Plugin/unraid/pages/scheduler.php +++ b/Plugin/unraid/pages/scheduler.php @@ -272,7 +272,7 @@ $runningScripts = array_unique($runningScripts);