Remember the last ten manual syncs so a repeat is one click
Retyping two paths and nine checkboxes correctly every time is where the mistakes come from, and "one-off" described how a transfer is scheduled rather than how often it is run. Pinned entries are exempt from the rotation — the command used twice a year is both the most worth keeping and the first that ten ordinary runs evict. Stored server-side, so the list is there from any screen, and loading one fills the form and stops rather than running it.
This commit is contained in:
@@ -8,10 +8,17 @@
|
||||
// OPERATIONAL MODEL
|
||||
// Entirely outside the profile system. The scheduled tiers sync configured shares with
|
||||
// configured profiles; this is for the one-off move that does not belong in a conf file —
|
||||
// seeding a new host, recovering a share, copying something once. Nothing here is recorded,
|
||||
// scheduled, or repeated.
|
||||
// seeding a new host, recovering a share, copying something once. Nothing here is scheduled.
|
||||
//
|
||||
// Six actions on one URL, in the order the panel uses them: hosts, browse, browse_local,
|
||||
// The last ten are remembered, which the header used to deny. "One-off" turned out to describe
|
||||
// how a transfer is scheduled, not how often it is run: seeding a host and recovering a share
|
||||
// are done repeatedly, and retyping two paths and nine checkboxes correctly each time is where
|
||||
// the mistakes come from. Pinned entries are exempt from the rotation, because the command used
|
||||
// twice a year is both the most valuable to keep and the first that ten ordinary runs evict.
|
||||
// Recording is a record of intent only — nothing re-runs itself, and loading an entry fills the
|
||||
// form and stops.
|
||||
//
|
||||
// Eight actions on one URL, in the order the panel uses them: recent, hosts, browse, browse_local,
|
||||
// run, poll, stop. run returns a token immediately and the transfer continues detached; poll
|
||||
// reads its log until a sentinel appears; stop kills it.
|
||||
//
|
||||
@@ -105,6 +112,9 @@
|
||||
// [user=root] [bw_limit=<KB/s>] [use_key=0|1] [flags=<rsync flags>]
|
||||
// GET ?action=poll&token=<hex16> output so far, and whether it finished
|
||||
// POST action=stop token=<hex16> cancel a running transfer
|
||||
// GET ?action=recent the remembered syncs, pinned first then newest
|
||||
// POST action=recent_update id=<hex12> op=rename|sticky|delete
|
||||
// [name=<label>] [value=0|1] rename, pin/unpin, or forget one entry
|
||||
//
|
||||
// RESPONSE
|
||||
// hosts {"ok":true,"hosts":[{slot,id,hostname,online,ip}],"has_key":bool,"ssh_key":"…"}
|
||||
@@ -319,6 +329,41 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'run') {
|
||||
. " rm -f " . escapeshellarg($pidFile);
|
||||
shell_exec('nohup bash -c ' . escapeshellarg($inner) . ' &>/dev/null &');
|
||||
|
||||
// Recorded at launch, not at completion. What is worth recalling is the command that was
|
||||
// assembled — a transfer that failed is often precisely the one to run again, and a poll that
|
||||
// never returns because the tab was closed would otherwise lose it entirely.
|
||||
//
|
||||
// Flags are stored without --bwlimit: it was appended above from its own field, and keeping it
|
||||
// in the list would restore it into the flag checkboxes where there is no such checkbox.
|
||||
$recent = vv_ms_recent_load();
|
||||
$key = vv_ms_recent_key($local, $slot, $remotePath);
|
||||
$entry = [
|
||||
'id' => $key,
|
||||
'ts' => time(),
|
||||
'local' => $local,
|
||||
'slot' => $slot,
|
||||
'user' => $user,
|
||||
'rpath' => $remotePath,
|
||||
'flags' => array_values(array_filter($flagList, fn($f) => !str_starts_with($f, '--bwlimit='))),
|
||||
'bw' => $bwLimit,
|
||||
'use_key' => $useKey,
|
||||
'name' => '',
|
||||
'sticky' => false,
|
||||
];
|
||||
$found = false;
|
||||
foreach ($recent as $i => $r) {
|
||||
if (($r['id'] ?? '') !== $key) continue;
|
||||
// The same copy run again: refresh everything except what the operator chose about it.
|
||||
// A name and a pin are decisions about the entry, not properties of the last run.
|
||||
$entry['name'] = $r['name'] ?? '';
|
||||
$entry['sticky'] = !empty($r['sticky']);
|
||||
$recent[$i] = $entry;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
if (!$found) $recent[] = $entry;
|
||||
vv_ms_recent_save($recent);
|
||||
|
||||
echo json_encode(['ok' => true, 'token' => $token]);
|
||||
exit;
|
||||
}
|
||||
@@ -373,4 +418,91 @@ if ($action === 'poll') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── recent ────────────────────────────────────────────────────────────────────
|
||||
// The last handful of manual syncs, so a repeat is one click rather than retyping two paths and
|
||||
// nine checkboxes correctly. Kept on the server rather than in localStorage: this operator drives
|
||||
// the same install from two desktop screens and a phone, and a recall list that only exists in
|
||||
// the browser that made it is a recall list that is missing whenever it is wanted.
|
||||
//
|
||||
// Rotation is by recency with a hard cap, except for pinned rows, which never rotate out and do
|
||||
// not count against the cap. That is the whole reason pinning exists — a seeding command used
|
||||
// twice a year is exactly the one worth keeping and exactly the one ten ordinary runs would push
|
||||
// off the end.
|
||||
//
|
||||
// Identity is the transfer itself — source, host, destination — not the flags. Re-running the same
|
||||
// copy with --delete added is the same entry with different options, and keeping both would fill
|
||||
// the list with near-duplicates that differ in the one place nobody reads.
|
||||
function vv_ms_recent_path(): string {
|
||||
return rtrim((string)(vv_conf_vars()['STATE_DIR'] ?? STATE_DIR), '/') . '/manual_sync_recent.json';
|
||||
}
|
||||
|
||||
function vv_ms_recent_load(): array {
|
||||
$raw = @file_get_contents(vv_ms_recent_path());
|
||||
if ($raw === false) return [];
|
||||
$d = json_decode($raw, true);
|
||||
return is_array($d) ? $d : [];
|
||||
}
|
||||
|
||||
function vv_ms_recent_save(array $rows): bool {
|
||||
// Pinned first so the cap can never evict one, then newest, then trimmed.
|
||||
usort($rows, function ($a, $b) {
|
||||
$p = (int)!empty($b['sticky']) <=> (int)!empty($a['sticky']);
|
||||
return $p !== 0 ? $p : ((int)($b['ts'] ?? 0) <=> (int)($a['ts'] ?? 0));
|
||||
});
|
||||
$kept = [];
|
||||
$loose = 0;
|
||||
foreach ($rows as $r) {
|
||||
if (!empty($r['sticky'])) { $kept[] = $r; continue; }
|
||||
if ($loose >= 10) continue;
|
||||
$loose++;
|
||||
$kept[] = $r;
|
||||
}
|
||||
$path = vv_ms_recent_path();
|
||||
$tmp = $path . '.tmp';
|
||||
if (@file_put_contents($tmp, json_encode($kept, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) === false) {
|
||||
return false;
|
||||
}
|
||||
return @rename($tmp, $path);
|
||||
}
|
||||
|
||||
// Identity of a transfer, for de-duplicating repeats of the same copy.
|
||||
function vv_ms_recent_key(string $local, string $slot, string $rpath): string {
|
||||
return substr(hash('sha256', $local . '|' . $slot . '|' . $rpath), 0, 12);
|
||||
}
|
||||
|
||||
if ($action === 'recent') {
|
||||
echo json_encode(['ok' => true, 'rows' => array_values(vv_ms_recent_load())]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'recent_update') {
|
||||
$id = preg_replace('/[^a-f0-9]/', '', trim($_POST['id'] ?? ''));
|
||||
$op = trim($_POST['op'] ?? '');
|
||||
if ($id === '') { echo json_encode(['ok' => false, 'error' => 'Missing id']); exit; }
|
||||
|
||||
$rows = vv_ms_recent_load();
|
||||
$hit = false;
|
||||
foreach ($rows as $i => $r) {
|
||||
if (($r['id'] ?? '') !== $id) continue;
|
||||
$hit = true;
|
||||
if ($op === 'rename') {
|
||||
// Plain text only, and short. It is a label in a list, and anything richer is markup
|
||||
// waiting to be rendered somewhere that forgot to escape it.
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$name = preg_replace('/[^\p{L}\p{N} ._\-\/→>]/u', '', $name);
|
||||
$rows[$i]['name'] = mb_substr($name, 0, 60);
|
||||
} elseif ($op === 'sticky') {
|
||||
$rows[$i]['sticky'] = ($_POST['value'] ?? '0') === '1';
|
||||
} elseif ($op === 'delete') {
|
||||
unset($rows[$i]);
|
||||
} else {
|
||||
echo json_encode(['ok' => false, 'error' => 'Unknown op']); exit;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!$hit) { echo json_encode(['ok' => false, 'error' => 'No such entry']); exit; }
|
||||
echo json_encode(['ok' => vv_ms_recent_save(array_values($rows))]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => false, 'error' => 'Unknown action']);
|
||||
|
||||
Reference in New Issue
Block a user