the snapshot, with an ETag (304 when unchanged) * * Stored in tournaments/live/.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']]);