'Not authenticated'], 401); } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { json_response(['error' => 'POST only'], 405); } csrf_verify(); $body = json_decode((string)file_get_contents('php://input'), true) ?: []; $action = (string)($body['action'] ?? ''); $gallery = gallery_load((string)($body['slug'] ?? '')); if ($gallery === null) { json_response(['error' => 'Unknown gallery'], 404); } $slug = $gallery['slug']; switch ($action) { case 'presign': $name = basename((string)($body['name'] ?? '')); $name = preg_replace('/[^A-Za-z0-9._-]+/', '-', $name) ?: 'file'; $name = substr($name, 0, 120); // Random prefix avoids overwrites when two files share a name. $token = random_token(6); $key = "galleries/$slug/originals/$token-$name"; $thumb = "galleries/$slug/thumbs/$token-$name.jpg"; json_response([ 'key' => $key, 'thumb' => $thumb, 'put_original' => s3_presign_put($key, 3600), 'put_thumb' => s3_presign_put($thumb, 3600), ]); case 'register': $key = (string)($body['key'] ?? ''); $thumb = (string)($body['thumb'] ?? ''); if (!str_starts_with($key, "galleries/$slug/")) { json_response(['error' => 'Key does not belong to this gallery'], 400); } if ($thumb !== '' && !str_starts_with($thumb, "galleries/$slug/")) { json_response(['error' => 'Thumb key does not belong to this gallery'], 400); } // Re-load under current state to reduce lost updates between requests. $gallery = gallery_load($slug); $gallery['images'][] = [ 'key' => $key, 'thumb' => $thumb !== '' ? $thumb : null, 'name' => substr((string)($body['name'] ?? basename($key)), 0, 200), 'size' => (int)($body['size'] ?? 0), ]; gallery_save($gallery); json_response(['ok' => true, 'count' => count($gallery['images'])]); default: json_response(['error' => 'Unknown action'], 400); }