Document the PHP api layer and fix what documenting it exposed
Writing down what each endpoint actually guarantees made the places it didn't obvious — shell arguments reaching a crontab or a bash -c unescaped, master.conf written without tmp+rename, and conf edits that could be saved without ever being parsed.
This commit is contained in:
@@ -1,4 +1,85 @@
|
||||
<?php
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// PURPOSE
|
||||
// Rsync profile CRUD. Lists, creates, updates and deletes the named transfer profiles that
|
||||
// control how a given share is synced — rsync options, bandwidth cap, retry behaviour,
|
||||
// container stop/start lists and exclusions.
|
||||
//
|
||||
// OPERATIONAL MODEL
|
||||
// A profile is not stored as a record. It is one key spread across nine parallel
|
||||
// `declare -A PROFILE_*` associative arrays in master.conf — PROFILE_RSYNC_OPTS[name],
|
||||
// PROFILE_BW_LIMIT[name], and so on. That layout exists because the shell layer reads each
|
||||
// setting independently, and this endpoint's whole job is to present it as a record anyway:
|
||||
// read all nine, pivot by profile name, and on save write the same key back into each.
|
||||
//
|
||||
// Every write therefore touches nine array declarations at once, submitted as a single
|
||||
// change set so they land together. A profile that existed in some arrays and not others
|
||||
// would read back with silently missing settings.
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
// Create and update are the same operation.
|
||||
// Save sets the named key in every array whether or not it was already there. There is
|
||||
// no separate create path, and therefore no way for the two to diverge on what a
|
||||
// complete profile looks like.
|
||||
//
|
||||
// Delete only touches arrays that actually contain the profile.
|
||||
// array_key_exists() is checked per array, and an empty change set is reported as
|
||||
// "Profile not found" rather than as a successful no-op.
|
||||
//
|
||||
// Values are quoted only when they need to be.
|
||||
// _rp_build_assoc() emits a bare value unless it is empty or contains whitespace or a
|
||||
// shell metacharacter. That keeps master.conf readable by hand, which is the reason the
|
||||
// whole config layer is bash rather than JSON.
|
||||
//
|
||||
// Missing arrays are skipped, not created.
|
||||
// A PROFILE_* declaration absent from master.conf is passed over. This endpoint edits
|
||||
// the schema that exists; adding a new setting is a conf template change, not a runtime
|
||||
// one.
|
||||
//
|
||||
// OPERATIONAL SAFEGUARDS
|
||||
// The profile name is constrained to characters that cannot break the array.
|
||||
// ^[a-zA-Z0-9_\-]+$ on both save and delete — no spaces, quotes, brackets or shell
|
||||
// metacharacters. The name becomes an associative-array subscript, so anything outside
|
||||
// that set could terminate the key or the declaration.
|
||||
//
|
||||
// Values are escaped on the way in, and the result is parsed before it lands.
|
||||
// Embedded quotes are backslash-escaped by _rp_build_assoc(), and
|
||||
// vv_conf_write_changes() then runs `bash -n` over the rewritten file. The escaping
|
||||
// handles the expected case; the syntax gate is what catches the unexpected one — and
|
||||
// it matters here because assoc_array values are spliced in verbatim rather than
|
||||
// through the scalar path's escaping.
|
||||
//
|
||||
// Writes go through the shared conf writer, so they are atomic — tmp + rename — and a
|
||||
// script sourcing master.conf mid-save sees the old file or the new one.
|
||||
//
|
||||
// The push happens only after every array write succeeded.
|
||||
// Guarded on the combined result, so a partially failed change set is not distributed
|
||||
// to partners. Profiles are shared configuration; a partner holding a different
|
||||
// definition of a profile would sync the same share differently.
|
||||
//
|
||||
// Save and delete are POST-only; only list is reachable by GET.
|
||||
//
|
||||
// Unknown actions fall through to an explicit error rather than an empty 200.
|
||||
//
|
||||
// REQUEST
|
||||
// GET|POST ?action=list all profiles, pivoted into records
|
||||
// POST action=save name=<profile> rsync_opts, bw_limit, retry_count, sleep,
|
||||
// critical_containers, delayed_containers, container_delay,
|
||||
// exclude_dirs, remote_restart (all optional, default empty)
|
||||
// POST action=delete name=<profile>
|
||||
//
|
||||
// RESPONSE
|
||||
// list {"ok":true,"profiles":{"<name>":{"<field>":"<value>", …}, …}}
|
||||
// save {"ok":bool,"results":{"master.conf":bool}}
|
||||
// delete {"ok":bool}
|
||||
// {"ok":false,"error":"Invalid profile name — …"|"No profile arrays found in master.conf"
|
||||
// |"Profile not found"|"Unknown action"}
|
||||
//
|
||||
// DEPENDS ON
|
||||
// include/confform.php vv_conf_write_changes()
|
||||
// include/config.php vv_read_conf_raw(), vv_push_master_conf(), vv_push_setup_state()
|
||||
// Rsync/rsync.sh consumer of every PROFILE_* array this writes
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
header('Content-Type: application/json');
|
||||
header('Cache-Control: no-store, no-cache');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
Reference in New Issue
Block a user