api/live.php
Rama main · réplica de lectura del repositorio original.
<?php
/**
* Live copy API: a slim snapshot of the tournament being played (Docs/HUB-DIRECTION.md, step 3).
*
* POST { id, rev, snapshot } store the snapshot (key required). Overwrites the one before,
* unless it carries an older rev (a late arrival): then nothing
* is changed and the answer says stale.
* POST { id, delete: true } remove the live copy (key required)
* GET { live: [ { id, name, date, status, rev, updatedAt, matches, played } ] }
* GET ?id=<id> the snapshot, with an ETag (304 when unchanged)
*
* Stored in tournaments/live/<id>.json: a subfolder that list-tournaments.php (which reads only
* tournaments/*.json) never lists, so Analytics cannot import a tournament that is still being
* played. Reading never needs the key, as elsewhere; the setting that sends it says so.
* A live copy not written to for three days is removed (an abandoned tournament).
*/
require_once 'api-check.php';
header('Content-Type: application/json');
header('Cache-Control: no-cache');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
header('Access-Control-Allow-Headers: Content-Type, X-API-Key, If-None-Match');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
$liveDir = dirname(__DIR__) . '/tournaments/live/';
const LIVE_MAX_BYTES = 1048576; // a snapshot is a few tens of KB; this is a ceiling, not a target
const LIVE_KEEP_SECONDS = 259200; // three days
/** A tournament id is a number (Date.now()); nothing else ever reaches the file system. */
function live_valid_id($id) {
return is_string($id) && preg_match('/^[0-9]{1,20}$/', $id) === 1;
}
/** Remove live copies nobody has written to for a while. */
function live_prune($dir) {
foreach (glob($dir . '*.json') ?: [] as $file) {
if (time() - (int)@filemtime($file) > LIVE_KEEP_SECONDS) @unlink($file);
}
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$id = isset($_GET['id']) ? (string)$_GET['id'] : null;
if ($id !== null) {
if (!live_valid_id($id)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid id']);
exit;
}
$file = $liveDir . $id . '.json';
if (!is_file($file)) {
http_response_code(404);
echo json_encode(['error' => 'No live copy of that tournament']);
exit;
}
$etag = '"' . filemtime($file) . '-' . filesize($file) . '"';
header('ETag: ' . $etag);
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && trim($_SERVER['HTTP_IF_NONE_MATCH']) === $etag) {
http_response_code(304);
exit;
}
readfile($file);
exit;
}
$live = [];
foreach (glob($liveDir . '*.json') ?: [] as $file) {
if (time() - (int)@filemtime($file) > LIVE_KEEP_SECONDS) continue;
$data = json_decode((string)@file_get_contents($file), true);
if (!is_array($data)) continue;
$live[] = [
'id' => (string)($data['id'] ?? basename($file, '.json')),
'name' => $data['name'] ?? 'Unknown',
'date' => $data['date'] ?? 'Unknown',
'status' => $data['status'] ?? 'unknown',
'rev' => $data['rev'] ?? 0,
'updatedAt' => filemtime($file),
'matches' => is_array($data['matches'] ?? null) ? count($data['matches']) : 0,
'played' => is_array($data['matches'] ?? null)
? count(array_filter($data['matches'], function ($m) { return !empty($m['completed']) && empty($m['autoAdvanced']); }))
: 0
];
}
echo json_encode(['live' => $live, 'count' => count($live)]);
exit;
}
if ($method !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
require_api_key();
$raw = file_get_contents('php://input');
if ($raw === false || strlen($raw) > LIVE_MAX_BYTES) {
http_response_code(413);
echo json_encode(['error' => 'Snapshot too large']);
exit;
}
$request = json_decode($raw, true);
if (!is_array($request) || !live_valid_id(isset($request['id']) ? (string)$request['id'] : '')) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid id']);
exit;
}
$id = (string)$request['id'];
$file = $liveDir . $id . '.json';
if (!empty($request['delete'])) {
if (is_file($file)) @unlink($file);
echo json_encode(['success' => true, 'deleted' => true]);
exit;
}
$snapshot = $request['snapshot'] ?? null;
if (!is_array($snapshot) || !isset($snapshot['rev']) || !is_numeric($snapshot['rev'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing snapshot or rev']);
exit;
}
$snapshot['id'] = $id;
if (!is_dir($liveDir) && !@mkdir($liveDir, 0755, true) && !is_dir($liveDir)) {
http_response_code(500);
echo json_encode(['error' => 'Could not create the live directory']);
exit;
}
// A late arrival must not overwrite a newer state
if (is_file($file)) {
$existing = json_decode((string)@file_get_contents($file), true);
if (is_array($existing) && isset($existing['rev']) && $existing['rev'] >= $snapshot['rev']) {
echo json_encode(['success' => true, 'stale' => true, 'rev' => $existing['rev']]);
exit;
}
}
$tmp = $file . '.tmp';
if (file_put_contents($tmp, json_encode($snapshot), LOCK_EX) === false || !@rename($tmp, $file)) {
@unlink($tmp);
http_response_code(500);
echo json_encode(['error' => 'Failed to save the live copy']);
exit;
}
@chmod($file, 0644);
live_prune($liveDir);
echo json_encode(['success' => true, 'rev' => $snapshot['rev']]);