The mirrored folder layout never reached the container card, which reads Varaverk's own store and not folder.view3's

This commit is contained in:
Gmer4Lfe
2026-08-21 21:50:22 -04:00
parent 4808a57289
commit f5d6edd9d0
+130 -68
View File
@@ -11,7 +11,8 @@
// --export --containers=a,b,c on the OWNER. Reads folder.view3's docker.json, intersects // --export --containers=a,b,c on the OWNER. Reads folder.view3's docker.json, intersects
// each folder with the deployed list, prints a JSON plan. // each folder with the deployed list, prints a JSON plan.
// --import on the MIRROR. Reads that plan on stdin and upserts each // --import on the MIRROR. Reads that plan on stdin and upserts each
// folder by name into the mirror's own docker.json. // folder by name into BOTH the mirror's folder.view3 docker.json
// and Varaverk's own docker_folders.json.
// //
// Onboard Step 12 runs the pair. Nothing is assumed about the mirror's layout: folders it // Onboard Step 12 runs the pair. Nothing is assumed about the mirror's layout: folders it
// already has are matched by name and extended, never duplicated or replaced. // already has are matched by name and extended, never duplicated or replaced.
@@ -39,8 +40,13 @@
// //
// Atomic write: .vv.tmp then rename(), so folder.view3 never reads a truncated file. // Atomic write: .vv.tmp then rename(), so folder.view3 never reads a truncated file.
// //
// Absent plugin is a clean skip, not an error. A mirror without folder.view3 installed has // Both stores or the layout is only half real. The Monitor container card reads Varaverk's
// nowhere to put folders and that is not a failure of the onboard. // docker_folders.json, never folder.view3's, and include/docker.php seeds it exactly once —
// when the file is absent. A mirror onboarded before this step existed had already been
// seeded, so writing only folder.view3 left the card showing the old single lump forever.
//
// Absent plugin is no longer a skip on import. folder.view3 is optional; Varaverk's own
// store is not, and it is the one the card reads. Export still needs the plugin and says so.
// //
// Import trusts nothing about shape: every folder needs a non-empty name and an array of // Import trusts nothing about shape: every folder needs a non-empty name and an array of
// container names, and anything else in the payload is ignored rather than merged. // container names, and anything else in the payload is ignored rather than merged.
@@ -58,11 +64,21 @@
// import: one line per folder written, then a count // import: one line per folder written, then a count
// //
// DEPENDS ON // DEPENDS ON
// /boot/config/plugins/folder.view3/docker.json the layout both sides read // /boot/config/plugins/folder.view3/docker.json the third-party layout, optional on import
// SCRIPTS_DIR/docker_folders.json Varaverk's own layout — what the card reads
// Tools/fallback_folder.php still owns the "<Owner>-Fallback" folder // Tools/fallback_folder.php still owns the "<Owner>-Fallback" folder
// ═══════════════════════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════════════════════
$pluginDir = dirname(__DIR__);
require_once $pluginDir . '/include/config.php';
define('FV3_JSON', '/boot/config/plugins/folder.view3/docker.json'); define('FV3_JSON', '/boot/config/plugins/folder.view3/docker.json');
// Varaverk's own store. Written as well as folder.view3's, because the Monitor container card
// reads THIS file and nothing else — include/docker.php imports folder.view3 exactly once, when
// this file does not yet exist. On a mirror onboarded before the layout step existed, that
// bootstrap had already happened, so the card went on showing one lump "<Owner>-Fallback"
// holding the entire auth and arr stacks while folder.view3 next to it showed the real shelves.
define('VV_STORE', SCRIPTS_DIR . '/docker_folders.json');
$opts = getopt('', ['export', 'import', 'containers:', 'fallback-only:', 'dry-run']); $opts = getopt('', ['export', 'import', 'containers:', 'fallback-only:', 'dry-run']);
@@ -72,15 +88,18 @@ $listArg = function (string $k) use ($opts): array {
}; };
// ── Load ───────────────────────────────────────────────────────────────────────────────────── // ── Load ─────────────────────────────────────────────────────────────────────────────────────
if (!file_exists(FV3_JSON)) { // folder.view3 is optional on the mirror. Export needs it and has nothing to say without it;
// Export has nothing to read; import has nowhere to write. Both are clean no-ops. // import does not — Varaverk's own store is the one the card reads, and it is always writable.
if (isset($opts['export'])) { echo json_encode(['folders' => [], 'unfiled' => $listArg('containers')]) . "\n"; exit(0); } $haveFv3 = file_exists(FV3_JSON);
echo "folder.view3 is not installed — nothing to do\n"; if (!$haveFv3 && isset($opts['export'])) {
echo json_encode(['folders' => [], 'unfiled' => $listArg('containers')]) . "\n";
exit(0); exit(0);
} }
$fv3 = json_decode((string)@file_get_contents(FV3_JSON), true); $fv3 = [];
if (!is_array($fv3)) { fwrite(STDERR, "folder.view3 docker.json is unreadable\n"); exit(1); } if ($haveFv3) {
$fv3 = json_decode((string)@file_get_contents(FV3_JSON), true);
if (!is_array($fv3)) { fwrite(STDERR, "folder.view3 docker.json is unreadable\n"); exit(1); }
}
// ── Export ─────────────────────────────────────────────────────────────────────────────────── // ── Export ───────────────────────────────────────────────────────────────────────────────────
if (isset($opts['export'])) { if (isset($opts['export'])) {
$deployed = $listArg('containers'); $deployed = $listArg('containers');
@@ -117,6 +136,7 @@ if (isset($opts['export'])) {
exit(0); exit(0);
} }
// ── Import ─────────────────────────────────────────────────────────────────────────────────── // ── Import ───────────────────────────────────────────────────────────────────────────────────
if (!isset($opts['import'])) { if (!isset($opts['import'])) {
fwrite(STDERR, "usage: mirror_folders.php --export --containers=… | --import\n"); fwrite(STDERR, "usage: mirror_folders.php --export --containers=… | --import\n");
@@ -130,71 +150,113 @@ if (!is_array($plan) || !isset($plan['folders']) || !is_array($plan['folders']))
exit(1); exit(1);
} }
$written = 0; // Applied to both stores, so they cannot drift apart the way they already did once. Takes a
foreach ($plan['folders'] as $spec) { // store, returns the store with the plan folded in plus what changed — no writing, no printing,
if (!is_array($spec)) continue; // because the two callers report differently.
$name = trim((string)($spec['name'] ?? '')); $applyPlan = function (array $store) use ($plan): array {
$cs = array_values(array_filter(array_map('trim', (array)($spec['containers'] ?? [])), 'strlen')); $written = 0;
if ($name === '' || !$cs) continue; $lines = [];
foreach ($plan['folders'] as $spec) {
if (!is_array($spec)) continue;
$name = trim((string)($spec['name'] ?? ''));
$cs = array_values(array_filter(array_map('trim', (array)($spec['containers'] ?? [])), 'strlen'));
if ($name === '' || !$cs) continue;
$targetId = null; $targetId = null;
foreach ($fv3 as $id => $f) { foreach ($store as $id => $f) {
if (is_array($f) && strcasecmp((string)($f['name'] ?? ''), $name) === 0) { $targetId = $id; break; } if (is_array($f) && strcasecmp((string)($f['name'] ?? ''), $name) === 0) { $targetId = $id; break; }
}
$created = false;
if ($targetId === null) {
// folder.view3's own id shape: 20 chars of url-safe base64.
$targetId = substr(str_replace(['+', '/', '='], '', base64_encode(random_bytes(15))), 0, 20);
$store[$targetId] = ['name' => $name, 'icon' => '', 'settings' => ['', '', '1', '', '1', ''],
'regex' => '', 'containers' => [], 'containerImages' => []];
$created = true;
}
$store[$targetId]['name'] = $name;
// Only fill an icon that is missing — a mirror that has styled its own folder keeps its choice.
if (($spec['icon'] ?? '') !== '' && trim((string)($store[$targetId]['icon'] ?? '')) === '') {
$store[$targetId]['icon'] = (string)$spec['icon'];
}
$have = (array)($store[$targetId]['containers'] ?? []);
$lc = array_map('strtolower', array_map('strval', $have));
$added = 0;
foreach ($cs as $c) {
if (in_array(strtolower($c), $lc, true)) continue;
$have[] = $c; $lc[] = strtolower($c); $added++;
}
$store[$targetId]['containers'] = array_values($have);
$lines[] = sprintf(" %-7s %-22s +%d (%s)", $created ? 'create' : 'update', $name, $added, implode(', ', $cs));
$written++;
} }
$created = false;
if ($targetId === null) { // ── Take the filed containers back out of any "-Fallback" folder ─────────────────────────
// folder.view3's own id shape: 20 chars of url-safe base64. // A container that now sits in "Arrs Stack" must not also sit in "Gmer4Lfe-Fallback". The
$targetId = substr(str_replace(['+', '/', '='], '', base64_encode(random_bytes(15))), 0, 20); // fallback folder answers "what is this host covering for the owner", and an earlier Step 12
$fv3[$targetId] = ['name' => $name, 'icon' => '', 'settings' => ['', '', '1', '', '1', ''], // dumped every deployed container into it — so it claimed the whole auth and arr stacks,
'regex' => '', 'containers' => [], 'containerImages' => []]; // which run here continuously and are not failover coverage at all.
$created = true; //
// Only containers this plan just filed are removed. Anything the operator put in that folder
// by hand, or that the onboard filed there deliberately as fallback-only, is left alone.
$filed = [];
foreach ($plan['folders'] as $spec) {
foreach ((array)($spec['containers'] ?? []) as $c) $filed[strtolower(trim((string)$c))] = true;
} }
$fv3[$targetId]['name'] = $name; $pruned = 0;
// Only fill an icon that is missing — a mirror that has styled its own folder keeps its choice. foreach ($store as $id => $f) {
if (($spec['icon'] ?? '') !== '' && trim((string)($fv3[$targetId]['icon'] ?? '')) === '') { if (!is_array($f) || !preg_match('/-Fallback$/i', (string)($f['name'] ?? ''))) continue;
$fv3[$targetId]['icon'] = (string)$spec['icon']; $keep = [];
foreach ((array)($f['containers'] ?? []) as $c) {
if (isset($filed[strtolower(trim((string)$c))])) { $pruned++; continue; }
$keep[] = $c;
}
$store[$id]['containers'] = array_values($keep);
} }
$have = (array)($fv3[$targetId]['containers'] ?? []); return [$store, $written, $pruned, $lines];
$lc = array_map('strtolower', array_map('strval', $have)); };
$added = 0;
foreach ($cs as $c) { $dryRun = isset($opts['dry-run']);
if (in_array(strtolower($c), $lc, true)) continue;
$have[] = $c; $lc[] = strtolower($c); $added++; // ── folder.view3's file ──────────────────────────────────────────────────────────────────────
$fvWritten = 0;
if ($haveFv3) {
[$fv3, $fvWritten, $fvPruned, $fvLines] = $applyPlan($fv3);
foreach ($fvLines as $l) echo $l . "\n";
if ($fvPruned) printf(" pruned %d container(s) from -Fallback folder(s) — they are filed properly now\n", $fvPruned);
if (($fvWritten || $fvPruned) && !$dryRun) {
$tmp = FV3_JSON . '.vv.tmp';
if (file_put_contents($tmp, json_encode($fv3, JSON_UNESCAPED_SLASHES)) === false || !rename($tmp, FV3_JSON)) {
fwrite(STDERR, "failed to write " . FV3_JSON . "\n"); exit(1);
}
printf(" %d folder(s) written to folder.view3 ✅\n", $fvWritten);
} }
$fv3[$targetId]['containers'] = array_values($have); } else {
printf(" %-7s %-22s +%d (%s)\n", $created ? 'create' : 'update', $name, $added, implode(', ', $cs)); echo " folder.view3 not installed — Varaverk's own layout only\n";
$written++;
} }
// ── Take the filed containers back out of any "-Fallback" folder ───────────────────────────── // ── Varaverk's own store ─────────────────────────────────────────────────────────────────────
// A container that now sits in "Arrs Stack" must not also sit in "Gmer4Lfe-Fallback". The // Seeded from folder.view3 when it does not exist yet, which is the same bootstrap
// fallback folder answers "what is this host covering for the owner", and an earlier Step 12 // include/docker.php performs — done here too so the very first import lands on a real layout
// dumped every deployed container into it — so it claimed the whole auth and arr stacks, which // rather than an empty file.
// run here continuously and are not failover coverage at all. $vvStore = [];
// if (file_exists(VV_STORE)) {
// Only containers this plan just filed are removed. Anything the operator put in that folder by $vvStore = json_decode((string)@file_get_contents(VV_STORE), true);
// hand, or that the onboard filed there deliberately as fallback-only, is left alone. if (!is_array($vvStore)) $vvStore = [];
$filed = []; } elseif ($haveFv3) {
foreach ($plan['folders'] as $spec) { $vvStore = $fv3;
foreach ((array)($spec['containers'] ?? []) as $c) $filed[strtolower(trim((string)$c))] = true;
} }
$pruned = 0; [$vvStore, $vvWritten, $vvPruned, ] = $applyPlan($vvStore);
foreach ($fv3 as $id => $f) { if (($vvWritten || $vvPruned) && !$dryRun) {
if (!is_array($f) || !preg_match('/-Fallback$/i', (string)($f['name'] ?? ''))) continue; @mkdir(dirname(VV_STORE), 0755, true);
$keep = []; $tmp = VV_STORE . '.vv.tmp';
foreach ((array)($f['containers'] ?? []) as $c) { if (file_put_contents($tmp, json_encode($vvStore, JSON_UNESCAPED_SLASHES)) === false || !rename($tmp, VV_STORE)) {
if (isset($filed[strtolower(trim((string)$c))])) { $pruned++; continue; } fwrite(STDERR, "failed to write " . VV_STORE . "\n"); exit(1);
$keep[] = $c;
} }
if ($pruned) $fv3[$id]['containers'] = array_values($keep);
} }
if ($pruned) printf(" pruned %d container(s) from -Fallback folder(s) — they are filed properly now\n", $pruned); printf(" %d folder(s) %s Varaverk's layout%s\n", $vvWritten,
$dryRun ? 'would be written to' : 'written to',
$vvPruned ? sprintf(" (%d unfiled from -Fallback)", $vvPruned) : '');
if (!$written && !$pruned) { echo " nothing to write\n"; exit(0); } if ($dryRun) echo " DRY RUN — nothing written\n";
if (isset($opts['dry-run'])) { echo " DRY RUN — nothing written\n"; exit(0); } if (!$fvWritten && !$vvWritten) echo " nothing to write\n";
$tmp = FV3_JSON . '.vv.tmp';
if (file_put_contents($tmp, json_encode($fv3, JSON_UNESCAPED_SLASHES)) === false || !rename($tmp, FV3_JSON)) {
fwrite(STDERR, "failed to write " . FV3_JSON . "\n"); exit(1);
}
printf(" %d folder(s) written ✅\n", $written);