'Not authenticated'], 401); } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { json_response(['error' => 'POST only'], 405); } csrf_verify(); // Release the session lock before the slow S3 leg, exactly as the uploader // does: PHP holds it for the whole request, and the admin would otherwise not // be able to load another page while a build is running. session_write_close(); @set_time_limit(0); // honoured on some hosts; the slice budget is the real cap $slug = (string)($_POST['slug'] ?? ''); $gallery = gallery_load($slug); if ($gallery === null) { json_response(['error' => 'Unknown gallery'], 404); } $startedAt = microtime(true); // One build at a time site-wide, shared with the background worker — otherwise // a manual rebuild and a background one would fight over the same state file. // Wait a little rather than failing instantly: the worker holds the lock through // short sleeps while a gallery settles, and an admin who asked for this should // not lose a race to one of them. $lock = archive_lock(15); if ($lock === null) { json_response(['error' => 'A background rebuild is running. It will finish on its own — or try again in a moment.'], 409); } // Whatever the wait above cost comes out of the slice, so the whole request // still fits inside one step_seconds and cannot drift towards the 60 s cap. $budget = max(5.0, (float)config('archive.step_seconds', 25) - (microtime(true) - $startedAt)); switch ((string)($_POST['action'] ?? '')) { case 'start': archive_abort($slug); // discard any half-finished attempt json_response(archive_run_slice($slug, $budget)); case 'step': json_response(archive_run_slice($slug, $budget)); case 'cancel': archive_abort($slug); json_response(['ok' => true]); case 'delete': archive_delete($slug); json_response(['ok' => true]); default: json_response(['error' => 'Unknown action'], 400); }