- Delete an asset * POST /api/projects/save - Save project data (JSON body) * GET /api/projects/list - List all saved projects * GET /api/projects/ - Load a specific project * DELETE /api/projects/ - Delete a project */ // --- Configuration --- define('STORAGE_DIR', __DIR__ . '/../storage'); define('ASSETS_DIR', STORAGE_DIR . '/assets'); define('PROJECTS_DIR', STORAGE_DIR . '/projects'); define('TMP_DIR', STORAGE_DIR . '/tmp'); define('MAX_UPLOAD_SIZE', 500 * 1024 * 1024); // 500MB // --- CORS & Headers --- header('Content-Type: application/json'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type'); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; } // --- Ensure storage directories --- foreach ([STORAGE_DIR, ASSETS_DIR, PROJECTS_DIR, TMP_DIR] as $dir) { if (!is_dir($dir)) { mkdir($dir, 0755, true); } } // --- Routing --- // Determine the API path from the request URI $requestUri = $_SERVER['REQUEST_URI'] ?? ''; $path = parse_url($requestUri, PHP_URL_PATH); // Strip trailing slash for consistency $path = rtrim($path, '/'); $method = $_SERVER['REQUEST_METHOD']; // Route the request if ($path === '/api/health') { handleHealth(); } elseif ($path === '/api/assets/upload' && $method === 'POST') { handleUpload(); } elseif ($path === '/api/assets' && $method === 'GET') { handleListAssets(); } elseif (preg_match('#^/api/assets/(.+)$#', $path, $m) && $method === 'DELETE') { handleDeleteAsset(urldecode($m[1])); } elseif ($path === '/api/projects/save' && ($method === 'POST' || $method === 'PUT')) { handleSaveProject(); } elseif ($path === '/api/projects/list' && $method === 'GET') { handleListProjects(); } elseif (preg_match('#^/api/projects/(.+)$#', $path, $m) && $method === 'GET') { handleGetProject(urldecode($m[1])); } elseif (preg_match('#^/api/projects/(.+)$#', $path, $m) && $method === 'DELETE') { handleDeleteProject(urldecode($m[1])); } else { sendError('Not Found', 404); } // =========================================== // Helper Functions // =========================================== function sendJson($data, $status = 200) { http_response_code($status); echo json_encode($data); exit; } function sendError($message, $status = 400) { sendJson(['success' => false, 'error' => $message], $status); } function generateUniqueFilename($originalName) { $timestamp = round(microtime(true) * 1000); $rand = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, 6); // Sanitize original name $safeName = preg_replace('/[^\w.\-]/', '_', $originalName); $ext = pathinfo($safeName, PATHINFO_EXTENSION); $name = pathinfo($safeName, PATHINFO_FILENAME); if (empty($ext)) { $ext = 'bin'; } return "{$timestamp}_{$rand}_{$name}.{$ext}"; } function getAssetType($filename) { $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); $imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'ico', 'bmp', 'tiff']; $videoExts = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv']; $cssExts = ['css']; $jsExts = ['js']; if (in_array($ext, $imageExts)) return 'image'; if (in_array($ext, $videoExts)) return 'video'; if (in_array($ext, $cssExts)) return 'css'; if (in_array($ext, $jsExts)) return 'js'; return 'file'; } function getAssetInfo($filename) { $filepath = ASSETS_DIR . '/' . $filename; if (!is_file($filepath)) return null; $stat = stat($filepath); // Extract original name from naming scheme: timestamp_random_originalname $originalName = $filename; $parts = explode('_', $filename, 3); if (count($parts) >= 3) { $originalName = $parts[2]; } return [ 'id' => $filename, 'name' => $originalName, 'filename' => $filename, 'url' => '/storage/assets/' . rawurlencode($filename), 'type' => getAssetType($filename), 'size' => $stat['size'], 'added' => intval($stat['mtime'] * 1000) ]; } // =========================================== // Endpoint Handlers // =========================================== function handleHealth() { sendJson(['status' => 'ok', 'server' => 'site-builder']); } function handleUpload() { $contentLength = intval($_SERVER['CONTENT_LENGTH'] ?? 0); if ($contentLength > MAX_UPLOAD_SIZE) { sendError('File too large. Maximum size is 500MB.', 413); } if (empty($_FILES)) { sendError('No files uploaded. Expected multipart/form-data with a "file" field.', 400); } $uploaded = []; // Handle single file field named 'file' or multiple files $files = []; if (isset($_FILES['file'])) { // Could be single or array upload if (is_array($_FILES['file']['name'])) { for ($i = 0; $i < count($_FILES['file']['name']); $i++) { $files[] = [ 'name' => $_FILES['file']['name'][$i], 'tmp_name' => $_FILES['file']['tmp_name'][$i], 'error' => $_FILES['file']['error'][$i], 'size' => $_FILES['file']['size'][$i], ]; } } else { $files[] = $_FILES['file']; } } else { // Accept any file field name foreach ($_FILES as $fileField) { if (is_array($fileField['name'])) { for ($i = 0; $i < count($fileField['name']); $i++) { $files[] = [ 'name' => $fileField['name'][$i], 'tmp_name' => $fileField['tmp_name'][$i], 'error' => $fileField['error'][$i], 'size' => $fileField['size'][$i], ]; } } else { $files[] = $fileField; } } } foreach ($files as $file) { if ($file['error'] !== UPLOAD_ERR_OK) { continue; } if (empty($file['name'])) { continue; } $uniqueName = generateUniqueFilename($file['name']); $destPath = ASSETS_DIR . '/' . $uniqueName; if (move_uploaded_file($file['tmp_name'], $destPath)) { $info = getAssetInfo($uniqueName); if ($info) { $uploaded[] = $info; } } } if (!empty($uploaded)) { sendJson(['success' => true, 'assets' => $uploaded]); } else { sendError('No files were uploaded', 400); } } function handleListAssets() { $assets = []; if (is_dir(ASSETS_DIR)) { $entries = scandir(ASSETS_DIR); sort($entries); foreach ($entries as $filename) { if ($filename === '.' || $filename === '..') continue; $info = getAssetInfo($filename); if ($info) { $assets[] = $info; } } } sendJson(['success' => true, 'assets' => $assets]); } function handleDeleteAsset($filename) { if (empty($filename)) { sendError('No filename specified', 400); } // Security: prevent directory traversal $safeFilename = basename($filename); $filepath = ASSETS_DIR . '/' . $safeFilename; if (!is_file($filepath)) { sendError('Asset not found', 404); } if (unlink($filepath)) { sendJson(['success' => true, 'deleted' => $safeFilename]); } else { sendError('Failed to delete asset', 500); } } function handleSaveProject() { $body = file_get_contents('php://input'); $data = json_decode($body, true); if ($data === null) { sendError('Invalid JSON', 400); } $projectId = $data['id'] ?? null; if (empty($projectId)) { $projectId = 'project_' . time() . '_' . rand(1000, 9999); $data['id'] = $projectId; } // Sanitize project ID for filesystem $safeId = preg_replace('/[^\w.\-]/', '_', $projectId); $filepath = PROJECTS_DIR . '/' . $safeId . '.json'; // Add timestamps if (empty($data['created'])) { $data['created'] = round(microtime(true) * 1000); } $data['modified'] = round(microtime(true) * 1000); $json = json_encode($data, JSON_PRETTY_PRINT); if (file_put_contents($filepath, $json) !== false) { sendJson([ 'success' => true, 'project' => [ 'id' => $projectId, 'name' => $data['name'] ?? 'Untitled', 'modified' => $data['modified'] ] ]); } else { sendError('Failed to save project', 500); } } function handleListProjects() { $projects = []; if (is_dir(PROJECTS_DIR)) { $entries = scandir(PROJECTS_DIR); foreach ($entries as $filename) { if (!str_ends_with($filename, '.json')) continue; $filepath = PROJECTS_DIR . '/' . $filename; $content = @file_get_contents($filepath); if ($content === false) continue; $data = @json_decode($content, true); if ($data === null) continue; $projects[] = [ 'id' => $data['id'] ?? pathinfo($filename, PATHINFO_FILENAME), 'name' => $data['name'] ?? 'Untitled', 'modified' => $data['modified'] ?? 0, 'created' => $data['created'] ?? 0, ]; } } // Sort by modified date descending usort($projects, function($a, $b) { return ($b['modified'] ?? 0) - ($a['modified'] ?? 0); }); sendJson(['success' => true, 'projects' => $projects]); } function handleGetProject($projectId) { if (empty($projectId)) { handleListProjects(); return; } $safeId = preg_replace('/[^\w.\-]/', '_', $projectId); $filepath = PROJECTS_DIR . '/' . $safeId . '.json'; if (!is_file($filepath)) { sendError('Project not found', 404); } $content = file_get_contents($filepath); $data = json_decode($content, true); if ($data === null) { sendError('Failed to parse project data', 500); } sendJson(['success' => true, 'project' => $data]); } function handleDeleteProject($projectId) { if (empty($projectId)) { sendError('No project ID specified', 400); } $safeId = preg_replace('/[^\w.\-]/', '_', $projectId); $filepath = PROJECTS_DIR . '/' . $safeId . '.json'; if (!is_file($filepath)) { sendError('Project not found', 404); } if (unlink($filepath)) { sendJson(['success' => true, 'deleted' => $projectId]); } else { sendError('Failed to delete project', 500); } }