albumsPath = __DIR__.'/albums/'; $this->dataManager = new DataManager(); $this->auth = new SimpleAuth(); // Initialize error handling and validation $this->errorHandler = new ErrorHandler('logs/api_error.log', 'INFO', true); $this->inputValidator = new InputValidator($this->errorHandler); // Initialize data if needed try { $this->dataManager->initializeData(); } catch (Exception $e) { // Use new error handler for logging $this->errorHandler->logError( 'DataManager initialization failed: '.$e->getMessage(), 'CONFIGURATION_ERROR', ['exception' => get_class($e)], ['method' => 'constructor'] ); } } /** * Handle incoming API requests with comprehensive error handling * @return string JSON response */ public function handleRequest() { try { // Log incoming request for audit trail $this->errorHandler->logInfo('API Request', [ 'action' => $_GET['action'] ?? 'none', 'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown', 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown' ]); $action = $_GET['action'] ?? ''; $method = $_SERVER['REQUEST_METHOD']; // Validate action parameter if (empty($action)) { $errorResponse = $this->errorHandler->createErrorResponse( 'Action parameter is required', 'MISSING_REQUIRED_FIELD', ['parameter' => 'action'] ); return $this->sendResponse($errorResponse); } // Define allowed actions and their required methods $allowedActions = [ 'getAlbums' => ['GET'], 'getPhotos' => ['GET'], 'createAlbum' => ['POST'], 'updateAlbum' => ['POST'], 'deleteAlbum' => ['POST'], 'getTags' => ['GET'], 'createTag' => ['POST'], 'deleteTag' => ['POST'], 'updateAlbumTags' => ['POST'], 'uploadPhoto' => ['POST'], 'deletePhoto' => ['POST'], 'updateRSSSettings' => ['POST'], 'getConfig' => ['GET'], 'updateConfig' => ['POST'], 'getStorageStats' => ['GET'], 'cleanupOrphanedFiles' => ['POST'], 'generateThumbnails' => ['POST'], 'getAlbumStorageUsage' => ['GET'], 'checkAuth' => ['GET'] ]; // Validate action exists if (!isset($allowedActions[$action])) { $errorResponse = $this->errorHandler->createErrorResponse( 'Invalid action: '.$action, 'INVALID_PARAMETER', ['action' => $action, 'allowed_actions' => array_keys($allowedActions)] ); return $this->sendResponse($errorResponse); } // Validate HTTP method $allowedMethods = $allowedActions[$action]; if (!in_array($method, $allowedMethods)) { $errorResponse = $this->errorHandler->createErrorResponse( "Method {$method} not allowed for action {$action}. Allowed methods: ".implode(', ', $allowedMethods), 'METHOD_NOT_ALLOWED', ['method' => $method, 'action' => $action, 'allowed_methods' => $allowedMethods] ); return $this->sendResponse($errorResponse); } // Route to appropriate method switch ($action) { case 'getAlbums': return $this->getAlbums(); case 'getPhotos': return $this->getPhotos(); case 'createAlbum': $this->auth->requireAuth(); return $this->createAlbum(); case 'updateAlbum': $this->auth->requireAuth(); return $this->updateAlbum(); case 'deleteAlbum': $this->auth->requireAuth(); return $this->deleteAlbum(); case 'getTags': return $this->getTags(); case 'createTag': $this->auth->requireAuth(); return $this->createTag(); case 'deleteTag': $this->auth->requireAuth(); return $this->deleteTag(); case 'updateAlbumTags': $this->auth->requireAuth(); return $this->updateAlbumTags(); case 'uploadPhoto': $this->auth->requireAuth(); return $this->uploadPhoto(); case 'deletePhoto': $this->auth->requireAuth(); return $this->deletePhoto(); case 'updateRSSSettings': $this->auth->requireAuth(); return $this->updateRSSSettings(); case 'getConfig': return $this->getConfig(); case 'updateConfig': $this->auth->requireAuth(); return $this->updateConfig(); case 'getStorageStats': $this->auth->requireAuth(); return $this->getStorageStats(); case 'cleanupOrphanedFiles': $this->auth->requireAuth(); return $this->cleanupOrphanedFiles(); case 'generateThumbnails': $this->auth->requireAuth(); return $this->generateThumbnails(); case 'getAlbumStorageUsage': $this->auth->requireAuth(); return $this->getAlbumStorageUsage(); case 'checkAuth': return $this->checkAuthStatus(); default: $errorResponse = $this->errorHandler->createErrorResponse( 'Invalid action: "'.$action.'" (length: '.strlen($action).')', 'INVALID_ACTION' ); return $this->sendResponse($errorResponse); } } catch (Exception $e) { // Handle any unexpected exceptions $errorResponse = $this->errorHandler->handleException($e, 'handleRequest'); return $this->sendResponse($errorResponse); } } /** * Create new album with enhanced validation and error handling * @return string JSON response */ private function createAlbum() { try { // Get and validate JSON input $inputRaw = file_get_contents('php://input'); if ($inputRaw === false) { $errorResponse = $this->errorHandler->createErrorResponse( 'Failed to read request body', 'INVALID_INPUT' ); return $this->sendResponse($errorResponse); } // Validate JSON format $input = json_decode($inputRaw, true); if (json_last_error() !== JSON_ERROR_NONE) { $errorResponse = $this->errorHandler->createErrorResponse( 'Invalid JSON input: '.json_last_error_msg(), 'INVALID_JSON' ); return $this->sendResponse($errorResponse); } // Validate album creation input $validationError = $this->inputValidator->validateAlbumCreation($input); if ($validationError) { return $this->sendResponse($validationError); } // Sanitize input $title = trim($input['title']); $description = isset($input['description']) ? trim($input['description']) : ''; // Create album in database $albumId = $this->dataManager->createAlbum($title, $description); // Create physical album directory $albumPath = $this->albumsPath.$albumId; if (!is_dir($albumPath)) { if (!mkdir($albumPath, 0755, true)) { // Rollback database entry try { $this->dataManager->deleteAlbum($albumId); } catch (Exception $rollbackError) { $this->errorHandler->logError( 'Failed to rollback album creation: '.$rollbackError->getMessage(), 'DATABASE_ERROR', ['album_id' => $albumId], ['method' => 'createAlbum', 'operation' => 'rollback'] ); } $errorResponse = $this->errorHandler->createErrorResponse( 'Failed to create album directory', 'FILE_SYSTEM_ERROR', ['album_path' => $albumPath] ); return $this->sendResponse($errorResponse); } } // Get the created album data $album = $this->dataManager->getAlbum($albumId); if (!$album) { $errorResponse = $this->errorHandler->createErrorResponse( 'Album created but failed to retrieve data', 'DATABASE_ERROR', ['album_id' => $albumId] ); return $this->sendResponse($errorResponse); } $successResponse = $this->errorHandler->createSuccessResponse([ 'message' => 'Album created successfully', 'album' => $album ], 201, 'Album created successfully'); return $this->sendResponse($successResponse); } catch (Exception $e) { $errorResponse = $this->errorHandler->handleException($e, 'createAlbum'); return $this->sendResponse($errorResponse); } } /** * Update existing album with enhanced validation and error handling * @return string JSON response */ private function updateAlbum() { try { // Validate album ID parameter $albumId = $_GET['albumId'] ?? ''; $validationError = $this->inputValidator->validateAlbumId($albumId); if ($validationError) { return $this->sendResponse($validationError); } // Get and validate JSON input $inputRaw = file_get_contents('php://input'); if ($inputRaw === false) { $errorResponse = $this->errorHandler->createErrorResponse( 'Failed to read request body', 'INVALID_INPUT' ); return $this->sendResponse($errorResponse); } // Validate JSON format $input = json_decode($inputRaw, true); if (json_last_error() !== JSON_ERROR_NONE) { $errorResponse = $this->errorHandler->createErrorResponse( 'Invalid JSON input: '.json_last_error_msg(), 'INVALID_JSON' ); return $this->sendResponse($errorResponse); } // Validate album update input $validationError = $this->inputValidator->validateAlbumUpdate($input); if ($validationError) { return $this->sendResponse($validationError); } // Validate album exists $existingAlbum = $this->dataManager->getAlbum($albumId); if (!$existingAlbum) { $errorResponse = $this->errorHandler->createErrorResponse( 'Album not found', 'ALBUM_NOT_FOUND', ['album_id' => $albumId] ); return $this->sendResponse($errorResponse); } // Prepare sanitized update data $updateData = []; if (isset($input['title'])) { $updateData['title'] = trim($input['title']); } if (isset($input['description'])) { $updateData['description'] = trim($input['description']); } if (isset($input['is_rss_enabled'])) { $updateData['is_rss_enabled'] = (bool) $input['is_rss_enabled']; } // Update album $success = $this->dataManager->updateAlbum($albumId, $updateData); if (!$success) { $errorResponse = $this->errorHandler->createErrorResponse( 'Failed to update album', 'DATABASE_ERROR', ['album_id' => $albumId, 'update_data' => $updateData] ); return $this->sendResponse($errorResponse); } // Get updated album data $album = $this->dataManager->getAlbum($albumId); // Trigger RSS feed auto-update if RSS settings were changed or album is RSS-enabled if (isset($updateData['is_rss_enabled']) || !empty($album['is_rss_enabled'])) { $this->triggerRSSAutoUpdate(); } $successResponse = $this->errorHandler->createSuccessResponse([ 'message' => 'Album updated successfully', 'album' => $album ], 200, 'Album updated successfully'); return $this->sendResponse($successResponse); } catch (Exception $e) { $errorResponse = $this->errorHandler->handleException($e, 'updateAlbum'); return $this->sendResponse($errorResponse); } } /** * Delete a tag with enhanced validation and error handling * @return string JSON response */ private function deleteTag() { try { // รับ tagId จากคำขอ $tagId = $_GET['tagId'] ?? ''; // ตรวจสอบความถูกต้องของ tagId $validationError = $this->inputValidator->validateTagId($tagId); if ($validationError) { return $this->sendResponse($validationError); } // ตรวจสอบว่า tag มีอยู่จริง $existingTag = $this->dataManager->getTag($tagId); if (!$existingTag) { $errorResponse = $this->errorHandler->createErrorResponse( 'Tag not found', 'TAG_NOT_FOUND', ['tag_id' => $tagId] ); return $this->sendResponse($errorResponse); } // ลบ tag ออกจากฐานข้อมูล $success = $this->dataManager->deleteTag($tagId); if (!$success) { $errorResponse = $this->errorHandler->createErrorResponse( 'Failed to delete tag', 'DATABASE_ERROR', ['tag_id' => $tagId] ); return $this->sendResponse($errorResponse); } // ส่งคำตอบสำเร็จ $successResponse = $this->errorHandler->createSuccessResponse([ 'message' => 'Tag deleted successfully', 'tag_id' => $tagId ], 200, 'Tag deleted successfully'); return $this->sendResponse($successResponse); } catch (Exception $e) { // จัดการข้อผิดพลาดที่ไม่คาดคิด $errorResponse = $this->errorHandler->handleException($e, 'deleteTag'); return $this->sendResponse($errorResponse); } } /** * Delete album and cleanup photos with enhanced validation and error handling * @return string JSON response */ private function deleteAlbum() { try { // Validate album ID parameter $albumId = $_GET['albumId'] ?? ''; $validationError = $this->inputValidator->validateAlbumId($albumId); if ($validationError) { return $this->sendResponse($validationError); } // Validate album exists $existingAlbum = $this->dataManager->getAlbum($albumId); if (!$existingAlbum) { $errorResponse = $this->errorHandler->createErrorResponse( 'Album not found', 'ALBUM_NOT_FOUND', ['album_id' => $albumId] ); return $this->sendResponse($errorResponse); } // Delete physical album directory and all photos $albumPath = $this->albumsPath.$albumId; if (is_dir($albumPath)) { if (!$this->deleteDirectory($albumPath)) { $this->errorHandler->logWarning( 'Failed to delete album directory completely', ['album_id' => $albumId, 'album_path' => $albumPath] ); } } // Delete album from database $success = $this->dataManager->deleteAlbum($albumId); if (!$success) { $errorResponse = $this->errorHandler->createErrorResponse( 'Failed to delete album from database', 'DATABASE_ERROR', ['album_id' => $albumId] ); return $this->sendResponse($errorResponse); } $successResponse = $this->errorHandler->createSuccessResponse([ 'message' => 'Album deleted successfully', 'albumId' => $albumId ], 200, 'Album deleted successfully'); return $this->sendResponse($successResponse); } catch (Exception $e) { $errorResponse = $this->errorHandler->handleException($e, 'deleteAlbum'); return $this->sendResponse($errorResponse); } } /** * Get all available tags with enhanced error handling * @return string JSON response */ private function getTags() { try { $tags = $this->dataManager->getTags(); $successResponse = $this->errorHandler->createSuccessResponse([ 'tags' => $tags ], 200, 'Tags retrieved successfully'); return $this->sendResponse($successResponse); } catch (Exception $e) { $errorResponse = $this->errorHandler->handleException($e, 'getTags'); return $this->sendResponse($errorResponse); } } /** * Create new tag with enhanced validation and error handling * @return string JSON response */ private function createTag() { try { // Get and validate JSON input $inputRaw = file_get_contents('php://input'); if ($inputRaw === false) { $errorResponse = $this->errorHandler->createErrorResponse( 'Failed to read request body', 'INVALID_INPUT' ); return $this->sendResponse($errorResponse); } // Validate JSON format $input = json_decode($inputRaw, true); if (json_last_error() !== JSON_ERROR_NONE) { $errorResponse = $this->errorHandler->createErrorResponse( 'Invalid JSON input: '.json_last_error_msg(), 'INVALID_JSON' ); return $this->sendResponse($errorResponse); } // Validate tag creation input $validationError = $this->inputValidator->validateTagCreation($input); if ($validationError) { return $this->sendResponse($validationError); } // Sanitize input $name = trim($input['name']); $color = isset($input['color']) ? trim($input['color']) : '#3498db'; // Create tag in database $tagId = $this->dataManager->createTag($name, $color); // Get the created tag data $tags = $this->dataManager->getTags(); $createdTag = null; foreach ($tags as $tag) { if ($tag['id'] == $tagId) { $createdTag = $tag; break; } } if (!$createdTag) { $errorResponse = $this->errorHandler->createErrorResponse( 'Tag created but failed to retrieve data', 'DATABASE_ERROR', ['tag_id' => $tagId] ); return $this->sendResponse($errorResponse); } $successResponse = $this->errorHandler->createSuccessResponse([ 'message' => 'Tag created successfully', 'tag' => $createdTag ], 201, 'Tag created successfully'); return $this->sendResponse($successResponse); } catch (Exception $e) { $errorResponse = $this->errorHandler->handleException($e, 'createTag'); return $this->sendResponse($errorResponse); } } /** * Update album tags with enhanced validation and error handling * @return string JSON response */ private function updateAlbumTags() { try { // Validate album ID parameter $albumId = $_GET['albumId'] ?? ''; $validationError = $this->inputValidator->validateAlbumId($albumId); if ($validationError) { return $this->sendResponse($validationError); } // Get and validate JSON input $inputRaw = file_get_contents('php://input'); if ($inputRaw === false) { $errorResponse = $this->errorHandler->createErrorResponse( 'Failed to read request body', 'INVALID_INPUT' ); return $this->sendResponse($errorResponse); } // Validate JSON format $input = json_decode($inputRaw, true); if (json_last_error() !== JSON_ERROR_NONE) { $errorResponse = $this->errorHandler->createErrorResponse( 'Invalid JSON input: '.json_last_error_msg(), 'INVALID_JSON' ); return $this->sendResponse($errorResponse); } // Validate album tags update input $validationError = $this->inputValidator->validateAlbumTagsUpdate($input); if ($validationError) { return $this->sendResponse($validationError); } // Validate album exists $existingAlbum = $this->dataManager->getAlbum($albumId); if (!$existingAlbum) { $errorResponse = $this->errorHandler->createErrorResponse( 'Album not found', 'ALBUM_NOT_FOUND', ['album_id' => $albumId] ); return $this->sendResponse($errorResponse); } $tagIds = $input['tagIds']; // Validate all tag IDs exist $allTags = $this->dataManager->getTags(); $validTagIds = array_column($allTags, 'id'); foreach ($tagIds as $tagId) { if (!in_array((int) $tagId, $validTagIds)) { $errorResponse = $this->errorHandler->createErrorResponse( 'Invalid tag ID: '.$tagId, 'TAG_NOT_FOUND', ['tag_id' => $tagId, 'valid_tag_ids' => $validTagIds] ); return $this->sendResponse($errorResponse); } } // Update album tags $success = $this->dataManager->updateAlbumTags($albumId, $tagIds); if (!$success) { $errorResponse = $this->errorHandler->createErrorResponse( 'Failed to update album tags', 'DATABASE_ERROR', ['album_id' => $albumId, 'tag_ids' => $tagIds] ); return $this->sendResponse($errorResponse); } // Get updated album data with tags $album = $this->dataManager->getAlbum($albumId); $albumTags = $this->dataManager->getAlbumTags($albumId); $successResponse = $this->errorHandler->createSuccessResponse([ 'message' => 'Album tags updated successfully', 'album' => $album, 'tags' => $albumTags ], 200, 'Album tags updated successfully'); return $this->sendResponse($successResponse); } catch (Exception $e) { $errorResponse = $this->errorHandler->handleException($e, 'updateAlbumTags'); return $this->sendResponse($errorResponse); } } /** * Upload photos to album with enhanced validation and error handling * @return string JSON response */ private function uploadPhoto() { try { // Validate album ID and file upload parameters $albumId = $_GET['albumId'] ?? ''; $validationError = $this->inputValidator->validateFileUpload($_FILES, $albumId); if ($validationError) { return $this->sendResponse($validationError); } // Validate album exists $existingAlbum = $this->dataManager->getAlbum($albumId); if (!$existingAlbum) { $errorResponse = $this->errorHandler->createErrorResponse( 'Album not found', 'ALBUM_NOT_FOUND', ['album_id' => $albumId] ); return $this->sendResponse($errorResponse); } // Get configuration for upload processing $config = $this->dataManager->getAllConfig(); // Initialize image processor and file upload handler $imageProcessor = new ImageProcessor($config); $uploadHandler = new FileUploadHandler($config, $imageProcessor, $this->dataManager); // Check storage limits before processing $totalUploadSize = 0; $files = $_FILES['photos']; // Calculate total size of files to be uploaded if (is_array($files['size'])) { $totalUploadSize = array_sum($files['size']); } else { $totalUploadSize = $files['size']; } $storageCheck = $uploadHandler->checkStorageLimit($totalUploadSize); if (!$storageCheck['can_upload']) { $errorResponse = $this->errorHandler->createErrorResponse( 'Storage limit exceeded. Available space: '.$this->formatBytes($storageCheck['available_space'] ?? 0), 'STORAGE_FULL', [ 'available_space' => $storageCheck['available_space'] ?? 0, 'requested_space' => $totalUploadSize, 'current_usage' => $storageCheck['current_usage'] ?? 0 ] ); return $this->sendResponse($errorResponse); } // Process file uploads $uploadResult = $uploadHandler->handleUpload($albumId, $files); if (!$uploadResult['success']) { $errorResponse = $this->errorHandler->createErrorResponse( 'Upload failed: '.$uploadResult['error'], 'IMAGE_PROCESSING_ERROR', ['upload_result' => $uploadResult] ); return $this->sendResponse($errorResponse); } // Prepare response with detailed results $response = [ 'message' => 'Photos uploaded successfully', 'album_id' => $albumId, 'total_files' => $uploadResult['total_files'], 'successful_uploads' => $uploadResult['successful_uploads'], 'failed_uploads' => $uploadResult['total_files'] - $uploadResult['successful_uploads'], 'upload_results' => $uploadResult['results'] ]; // Add storage information if (isset($storageCheck['current_usage'])) { $response['storage_info'] = [ 'current_usage' => $storageCheck['current_usage'], 'max_size' => $storageCheck['max_size'] ?? null, 'usage_percentage' => $storageCheck['usage_percentage'] ?? null ]; } // Get updated album information $updatedAlbum = $this->dataManager->getAlbum($albumId); $response['album'] = $updatedAlbum; // Trigger RSS feed auto-update if album is RSS-enabled if (!empty($updatedAlbum['is_rss_enabled'])) { $this->triggerRSSAutoUpdate(); } $successResponse = $this->errorHandler->createSuccessResponse( $response, 200, 'Photos uploaded successfully' ); return $this->sendResponse($successResponse); } catch (Exception $e) { $errorResponse = $this->errorHandler->handleException($e, 'uploadPhoto'); return $this->sendResponse($errorResponse); } } /** * Delete a single photo from an album * @return string JSON response */ private function deletePhoto() { try { // Get parameters $albumId = $_GET['albumId'] ?? ''; $photoFilename = $_GET['filename'] ?? ''; // Validate album ID $validationError = $this->inputValidator->validateAlbumId($albumId); if ($validationError) { return $this->sendResponse($validationError); } // Validate filename if (empty($photoFilename)) { $errorResponse = $this->errorHandler->createErrorResponse( 'Photo filename is required', 'MISSING_REQUIRED_FIELD', ['field' => 'filename'] ); return $this->sendResponse($errorResponse); } // Validate album exists $existingAlbum = $this->dataManager->getAlbum($albumId); if (!$existingAlbum) { $errorResponse = $this->errorHandler->createErrorResponse( 'Album not found', 'ALBUM_NOT_FOUND', ['album_id' => $albumId] ); return $this->sendResponse($errorResponse); } // Get album photos to verify photo exists $photos = $this->dataManager->getPhotos($albumId); $photoToDelete = null; foreach ($photos as $photo) { if ($photo['filename'] === $photoFilename) { $photoToDelete = $photo; break; } } if (!$photoToDelete) { $errorResponse = $this->errorHandler->createErrorResponse( 'Photo not found in album', 'PHOTO_NOT_FOUND', ['album_id' => $albumId, 'filename' => $photoFilename] ); return $this->sendResponse($errorResponse); } // Delete physical file $photoPath = $this->albumsPath.$albumId.'/'.$photoFilename; if (file_exists($photoPath)) { if (!unlink($photoPath)) { $errorResponse = $this->errorHandler->createErrorResponse( 'Failed to delete photo file', 'FILE_SYSTEM_ERROR', ['photo_path' => $photoPath] ); return $this->sendResponse($errorResponse); } } // Remove photo from database $success = $this->dataManager->deletePhotoByFilename($albumId, $photoFilename); if (!$success) { $errorResponse = $this->errorHandler->createErrorResponse( 'Failed to delete photo from database', 'DATABASE_ERROR', ['album_id' => $albumId, 'filename' => $photoFilename] ); return $this->sendResponse($errorResponse); } $successResponse = $this->errorHandler->createSuccessResponse([ 'message' => 'Photo deleted successfully', 'albumId' => $albumId, 'filename' => $photoFilename ], 200, 'Photo deleted successfully'); return $this->sendResponse($successResponse); } catch (Exception $e) { $errorResponse = $this->errorHandler->handleException($e, 'deletePhoto'); return $this->sendResponse($errorResponse); } } /** * Recursively delete directory and all contents * @param string $dir Directory path * @return bool Success status */ private function deleteDirectory($dir) { if (!is_dir($dir)) { return false; } $files = array_diff(scandir($dir), ['.', '..']); foreach ($files as $file) { $path = $dir.'/'.$file; if (is_dir($path)) { $this->deleteDirectory($path); } else { unlink($path); } } return rmdir($dir); } /** * Get all albums with enhanced metadata including title, description, tags, and photo count * @return mixed JSON response with albums data */ private function getAlbums() { try { // Get query parameters for filtering and pagination $tagFilter = $_GET['tag'] ?? ''; // Input validation for tag filter if (!empty($tagFilter) && !is_numeric($tagFilter)) { $errorResponse = $this->errorHandler->createErrorResponse( 'Invalid tag filter. Tag ID must be numeric.', 'VALIDATION_FAILED', ['tag_filter' => $tagFilter] ); return $this->sendResponse($errorResponse); } // Try to get albums from DataManager first try { $dbAlbums = $this->dataManager->getAlbums(true); // Enhance each album with complete metadata foreach ($dbAlbums as &$album) { // Extract folder number from title (e.g., "Album 12" -> "12") $folderNumber = $album['id']; // Default to id if (preg_match('/Album (\d+)/', $album['title'], $matches)) { $folderNumber = $matches[1]; } // Get photos from filesystem to ensure accuracy $filesystemPhotos = $this->getAlbumPhotos($folderNumber); $dbPhotos = $this->dataManager->getPhotos($album['id']); // Use database photos if available, otherwise fallback to filesystem $photos = !empty($dbPhotos) ? $dbPhotos : array_map(function ($filename) use ($album) { return [ 'id' => null, 'filename' => $filename, 'original_filename' => $filename, 'file_size' => null, 'width' => null, 'height' => null, 'uploaded_at' => null ]; }, $filesystemPhotos); // Set cover photo (first uploaded image) $coverPhoto = null; if (!empty($photos)) { $coverPhoto = [ 'filename' => $photos[0]['filename'], 'path' => "albums/{$folderNumber}/{$photos[0]['filename']}" ]; } // Ensure all required fields are present $album['title'] = $album['title'] ?? "Album ".$album['id']; $album['description'] = $album['description'] ?? ''; $album['photo_count'] = count($photos); $album['photoCount'] = $album['photo_count']; // Backward compatibility $album['name'] = $album['title']; // Backward compatibility $album['cover_photo'] = $coverPhoto; $album['photos'] = !empty($photos) ? [$photos[0]] : []; // First photo for backward compatibility // Get detailed tag information $albumTags = $this->dataManager->getAlbumTags($album['id']); $album['tag_details'] = $albumTags; // Ensure tags array exists $album['tags'] = $album['tags'] ?? []; // Add timestamps if missing $album['created_at'] = $album['created_at'] ?? date('c'); $album['updated_at'] = $album['updated_at'] ?? date('c'); $album['is_rss_enabled'] = $album['is_rss_enabled'] ?? false; } // Apply tag filtering if specified if (!empty($tagFilter)) { $dbAlbums = array_filter($dbAlbums, function ($album) use ($tagFilter) { return in_array((int) $tagFilter, $album['tags']); }); } // Re-index array after filtering and sort by updated_at descending $dbAlbums = array_values($dbAlbums); usort($dbAlbums, function ($a, $b) { return strtotime($b['updated_at']) - strtotime($a['updated_at']); }); return $this->success([ 'albums' => $dbAlbums, 'total_count' => count($dbAlbums), 'filtered_by_tag' => !empty($tagFilter) ? (int) $tagFilter : null ]); } catch (Exception $e) { $errorResponse = $this->errorHandler->createErrorResponse( 'Failed to load albums from database: '.$e->getMessage(), 'DATABASE_ERROR', ['error_details' => $e->getMessage()] ); return $this->sendResponse($errorResponse); } } catch (Exception $e) { $errorResponse = $this->errorHandler->handleException($e, 'getAlbums'); return $this->sendResponse($errorResponse); } } /** * Get photos for album with enhanced metadata and pagination * @return mixed JSON response with photos data */ private function getPhotos() { try { // Get and validate query parameters $albumId = $_GET['albumId'] ?? ''; $page = (int) ($_GET['page'] ?? 1); $limit = (int) ($_GET['limit'] ?? 20); $sortBy = $_GET['sortBy'] ?? 'uploaded_at'; // uploaded_at, filename, file_size $sortOrder = $_GET['sortOrder'] ?? 'desc'; // asc, desc // Validate album ID parameter $validationError = $this->inputValidator->validateAlbumId($albumId); if ($validationError) { return $this->sendResponse($validationError); } // Validate pagination parameters $paginationParams = [ 'page' => $page, 'limit' => $limit, 'sortBy' => $sortBy, 'sortOrder' => $sortOrder ]; $validationError = $this->inputValidator->validatePaginationParams($paginationParams); if ($validationError) { return $this->sendResponse($validationError); } // Get album info to determine correct folder $album = $this->dataManager->getAlbum($albumId); if (!$album) { $errorResponse = $this->errorHandler->createErrorResponse( 'Album not found in database', 'ALBUM_NOT_FOUND', ['album_id' => $albumId] ); return $this->sendResponse($errorResponse); } // Extract folder number from title (e.g., "Album 12" -> "12") $folderNumber = $albumId; // Default to albumId if (preg_match('/Album (\d+)/', $album['title'], $matches)) { $folderNumber = $matches[1]; } // Validate album folder exists $albumPath = $this->albumsPath.$folderNumber.'/'; if (!is_dir($albumPath)) { $errorResponse = $this->errorHandler->createErrorResponse( 'Album folder not found', 'ALBUM_FOLDER_NOT_FOUND', ['album_id' => $albumId, 'folder_number' => $folderNumber, 'path' => $albumPath] ); return $this->sendResponse($errorResponse); } // Try to get photos from database first (with metadata) try { $dbPhotos = $this->dataManager->getPhotos($albumId); if (!empty($dbPhotos)) { // Enhance photos with additional metadata and file system verification $enhancedPhotos = []; foreach ($dbPhotos as $photo) { $filePath = $albumPath.$photo['filename']; // Verify file still exists on filesystem if (!file_exists($filePath)) { continue; // Skip missing files } // Get file system metadata $fileSize = filesize($filePath); $fileModTime = filemtime($filePath); // Get image dimensions if not stored in database $width = $photo['width']; $height = $photo['height']; if (empty($width) || empty($height)) { $imageInfo = getimagesize($filePath); if ($imageInfo !== false) { $width = $imageInfo[0]; $height = $imageInfo[1]; } } $enhancedPhoto = [ 'id' => $photo['id'], 'filename' => $photo['filename'], 'original_filename' => $photo['original_filename'] ?? $photo['filename'], 'file_size' => $photo['file_size'] ?? $fileSize, 'file_size_formatted' => $this->formatBytes($photo['file_size'] ?? $fileSize), 'width' => $width, 'height' => $height, 'aspect_ratio' => ($width && $height) ? round($width / $height, 2) : null, 'uploaded_at' => $photo['uploaded_at'], 'uploaded_at_formatted' => $photo['uploaded_at'] ? date('Y-m-d H:i:s', strtotime($photo['uploaded_at'])) : null, 'file_modified_at' => date('c', $fileModTime), 'path' => "albums/{$albumId}/{$photo['filename']}", 'thumbnail_path' => "albums/{$albumId}/thumb_{$photo['filename']}", 'has_thumbnail' => file_exists($albumPath.'thumb_'.$photo['filename']) ]; $enhancedPhotos[] = $enhancedPhoto; } // Sort photos based on parameters usort($enhancedPhotos, function ($a, $b) use ($sortBy, $sortOrder) { $valueA = $a[$sortBy] ?? ''; $valueB = $b[$sortBy] ?? ''; // Handle different data types if (in_array($sortBy, ['file_size', 'width', 'height'])) { $valueA = (int) $valueA; $valueB = (int) $valueB; } elseif ($sortBy === 'uploaded_at') { $valueA = strtotime($valueA ?: '1970-01-01'); $valueB = strtotime($valueB ?: '1970-01-01'); } $comparison = $valueA <=> $valueB; return $sortOrder === 'desc' ? -$comparison : $comparison; }); // Apply pagination $totalPhotos = count($enhancedPhotos); $offset = ($page - 1) * $limit; $paginatedPhotos = array_slice($enhancedPhotos, $offset, $limit); return $this->success([ 'photos' => $paginatedPhotos, 'pagination' => [ 'page' => $page, 'limit' => $limit, 'total_photos' => $totalPhotos, 'total_pages' => ceil($totalPhotos / $limit), 'has_more' => $offset + $limit < $totalPhotos, 'has_previous' => $page > 1 ], 'sorting' => [ 'sort_by' => $sortBy, 'sort_order' => $sortOrder ], 'album' => [ 'id' => $album['id'], 'title' => $album['title'], 'photo_count' => $totalPhotos ] ]); } } catch (Exception $e) { // Log error but continue with filesystem fallback error_log('DataManager getPhotos error: '.$e->getMessage()); } // Fallback to filesystem-based photo discovery with basic metadata $filesystemPhotos = $this->getAlbumPhotos($albumId); $enhancedPhotos = []; foreach ($filesystemPhotos as $filename) { $filePath = $albumPath.$filename; if (!file_exists($filePath)) { continue; } $fileSize = filesize($filePath); $fileModTime = filemtime($filePath); // Get image dimensions $width = null; $height = null; $imageInfo = getimagesize($filePath); if ($imageInfo !== false) { $width = $imageInfo[0]; $height = $imageInfo[1]; } $enhancedPhoto = [ 'id' => null, // No database ID for filesystem-only photos 'filename' => $filename, 'original_filename' => $filename, 'file_size' => $fileSize, 'file_size_formatted' => $this->formatBytes($fileSize), 'width' => $width, 'height' => $height, 'aspect_ratio' => ($width && $height) ? round($width / $height, 2) : null, 'uploaded_at' => null, 'uploaded_at_formatted' => null, 'file_modified_at' => date('c', $fileModTime), 'path' => "albums/{$albumId}/{$filename}", 'thumbnail_path' => "albums/{$albumId}/thumb_{$filename}", 'has_thumbnail' => file_exists($albumPath.'thumb_'.$filename) ]; $enhancedPhotos[] = $enhancedPhoto; } // Sort photos (filesystem fallback uses file modification time for uploaded_at) usort($enhancedPhotos, function ($a, $b) use ($sortBy, $sortOrder) { $valueA = $a[$sortBy] ?? ''; $valueB = $b[$sortBy] ?? ''; // Handle sorting by uploaded_at for filesystem photos if ($sortBy === 'uploaded_at') { $valueA = strtotime($a['file_modified_at']); $valueB = strtotime($b['file_modified_at']); } elseif (in_array($sortBy, ['file_size', 'width', 'height'])) { $valueA = (int) $valueA; $valueB = (int) $valueB; } $comparison = $valueA <=> $valueB; return $sortOrder === 'desc' ? -$comparison : $comparison; }); // Apply pagination $totalPhotos = count($enhancedPhotos); $offset = ($page - 1) * $limit; $paginatedPhotos = array_slice($enhancedPhotos, $offset, $limit); return $this->success([ 'photos' => $paginatedPhotos, 'pagination' => [ 'page' => $page, 'limit' => $limit, 'total_photos' => $totalPhotos, 'total_pages' => ceil($totalPhotos / $limit), 'has_more' => $offset + $limit < $totalPhotos, 'has_previous' => $page > 1 ], 'sorting' => [ 'sort_by' => $sortBy, 'sort_order' => $sortOrder ], 'album' => [ 'id' => $albumId, 'title' => "Album {$albumId}", 'photo_count' => $totalPhotos ] ]); } catch (Exception $e) { return $this->error('Error loading photos: '.$e->getMessage()); } } /** * @param $albumId * @return mixed */ private function getAlbumPhotos($albumId) { $albumPath = $this->albumsPath.$albumId.'/'; $photos = []; if (!is_dir($albumPath)) { return $photos; } $files = scandir($albumPath); foreach ($files as $file) { // Skip directories, thumbnail files, and non-image files if ($file === '.' || $file === '..' || is_dir($albumPath.$file) || strpos($file, 'thumb_') === 0 || !$this->isImageFile($file)) { continue; } $photos[] = $file; } // Sort photos naturally (1.jpg, 2.jpg, 10.jpg, etc.) natsort($photos); return array_values($photos); } /** * @param $filename */ private function isImageFile($filename) { $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif']; $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); return in_array($extension, $allowedExtensions); } /** * Return success response with proper structure * @param array $data Response data * @param int $httpCode HTTP status code * @return string JSON response */ private function success($data, $httpCode = 200) { http_response_code($httpCode); // Ensure consistent response structure $response = [ 'success' => true, 'timestamp' => date('c'), 'data' => $data ]; // Merge data at root level for backward compatibility return json_encode($response + $data); } /** * Return error response with proper structure and logging * @param string $message Error message * @param int $httpCode HTTP status code * @param string $errorCode Internal error code * @param array $details Additional error details * @return string JSON response */ private function error($message, $httpCode = 400, $errorCode = null, $details = []) { http_response_code($httpCode); // Log error for debugging $logMessage = "API Error: {$message}"; if ($errorCode) { $logMessage .= " (Code: {$errorCode})"; } if (!empty($details)) { $logMessage .= " Details: ".json_encode($details); } error_log($logMessage); $response = [ 'success' => false, 'error' => $message, 'timestamp' => date('c') ]; if ($errorCode) { $response['error_code'] = $errorCode; } if (!empty($details)) { $response['details'] = $details; } return json_encode($response); } /** * Format bytes to human readable format * @param int $bytes Number of bytes * @return string Formatted string */ private function formatBytes($bytes) { $units = ['B', 'KB', 'MB', 'GB']; $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= pow(1024, $pow); return round($bytes, 2).' '.$units[$pow]; } /** * Update RSS configuration settings * @return mixed JSON response */ private function updateRSSSettings() { try { // Get JSON input $input = json_decode(file_get_contents('php://input'), true); if (!$input) { return $this->error('Invalid JSON input'); } // Define allowed RSS configuration keys $allowedKeys = [ 'rss_title', 'rss_description', 'rss_max_items', 'rss_base_url' ]; $updatedSettings = []; foreach ($input as $key => $value) { if (in_array($key, $allowedKeys)) { // Validate specific settings switch ($key) { case 'rss_title': case 'rss_description': $value = trim($value); if (empty($value)) { return $this->error("$key cannot be empty"); } break; case 'rss_max_items': $value = (int) $value; if ($value < 1 || $value > 200) { return $this->error('rss_max_items must be between 1 and 200'); } break; case 'rss_base_url': $value = trim($value); if (empty($value) || !filter_var($value, FILTER_VALIDATE_URL)) { return $this->error('rss_base_url must be a valid URL'); } break; } // Update configuration $this->dataManager->setConfig($key, $value); $updatedSettings[$key] = $value; } } if (empty($updatedSettings)) { return $this->error('No valid RSS settings provided'); } return $this->success([ 'message' => 'RSS settings updated successfully', 'updated_settings' => $updatedSettings ]); } catch (Exception $e) { return $this->error('Failed to update RSS settings: '.$e->getMessage()); } } /** * Get configuration settings * @return mixed JSON response with configuration data */ private function getConfig() { try { // Get query parameter to filter specific config keys $keys = $_GET['keys'] ?? ''; $includeStorageInfo = isset($_GET['includeStorageInfo']) ? (bool) $_GET['includeStorageInfo'] : true; // Get all configuration $config = $this->dataManager->getAllConfig(); // Filter by specific keys if requested if (!empty($keys)) { $requestedKeys = array_map('trim', explode(',', $keys)); $filteredConfig = []; foreach ($requestedKeys as $key) { if (array_key_exists($key, $config)) { $filteredConfig[$key] = $config[$key]; } } $config = $filteredConfig; } // Add storage usage information if requested $storageInfo = null; if ($includeStorageInfo) { $storageInfo = $this->getStorageUsageInfo(); } // Organize configuration into logical groups $organizedConfig = [ 'upload' => [ 'max_file_size' => $config['upload_max_file_size'] ?? 10485760, 'max_file_size_formatted' => $this->formatBytes($config['upload_max_file_size'] ?? 10485760), 'allowed_types' => $config['upload_allowed_types'] ?? [] ], 'image_processing' => [ 'webp_quality' => $config['image_webp_quality'] ?? 80, 'max_width' => $config['image_max_width'] ?? 1000, 'max_height' => $config['image_max_height'] ?? 1000, 'thumbnail_size' => $config['image_thumbnail_size'] ?? 300 ], 'rss' => [ 'title' => $config['rss_title'] ?? 'Photo Gallery RSS Feed', 'description' => $config['rss_description'] ?? 'Latest photos from selected albums', 'max_items' => $config['rss_max_items'] ?? 50, 'base_url' => $config['rss_base_url'] ?? $this->getBaseUrl() ], 'storage' => [ 'max_total_size' => $config['storage_max_total_size'] ?? 1073741824, 'max_total_size_formatted' => $this->formatBytes($config['storage_max_total_size'] ?? 1073741824), 'cleanup_enabled' => $config['storage_cleanup_enabled'] ?? true ] ]; $response = [ 'config' => $organizedConfig, 'raw_config' => $config ]; if ($storageInfo) { $response['storage_info'] = $storageInfo; } return $this->success($response); } catch (Exception $e) { return $this->error('Failed to get configuration: '.$e->getMessage()); } } /** * Update configuration settings * @return mixed JSON response */ private function updateConfig() { try { // Get JSON input $inputRaw = file_get_contents('php://input'); if ($inputRaw === false) { return $this->error('Failed to read request body', 400, 'INPUT_READ_ERROR'); } $input = json_decode($inputRaw, true); if (json_last_error() !== JSON_ERROR_NONE) { return $this->error('Invalid JSON input: '.json_last_error_msg(), 400, 'INVALID_JSON'); } if (!is_array($input)) { return $this->error('Request body must be a JSON object', 400, 'INVALID_INPUT_TYPE'); } // Define allowed configuration keys with validation rules $allowedKeys = [ 'upload_max_file_size' => [ 'type' => 'integer', 'min' => 1048576, // 1MB minimum 'max' => 104857600, // 100MB maximum 'description' => 'Maximum file size for uploads (bytes)' ], 'upload_allowed_types' => [ 'type' => 'array', 'allowed_values' => ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff', 'image/webp'], 'description' => 'Allowed MIME types for uploads' ], 'image_webp_quality' => [ 'type' => 'integer', 'min' => 1, 'max' => 100, 'description' => 'WebP compression quality (1-100)' ], 'image_max_width' => [ 'type' => 'integer', 'min' => 100, 'max' => 5000, 'description' => 'Maximum image width in pixels' ], 'image_max_height' => [ 'type' => 'integer', 'min' => 100, 'max' => 5000, 'description' => 'Maximum image height in pixels' ], 'image_thumbnail_size' => [ 'type' => 'integer', 'min' => 50, 'max' => 500, 'description' => 'Thumbnail size in pixels' ], 'rss_title' => [ 'type' => 'string', 'max_length' => 255, 'description' => 'RSS feed title' ], 'rss_description' => [ 'type' => 'string', 'max_length' => 1000, 'description' => 'RSS feed description' ], 'rss_max_items' => [ 'type' => 'integer', 'min' => 1, 'max' => 200, 'description' => 'Maximum items in RSS feed' ], 'rss_base_url' => [ 'type' => 'url', 'description' => 'Base URL for RSS feed links' ], 'storage_max_total_size' => [ 'type' => 'integer', 'min' => 104857600, // 100MB minimum 'max' => 10737418240, // 10GB maximum 'description' => 'Maximum total storage size (bytes)' ], 'storage_cleanup_enabled' => [ 'type' => 'boolean', 'description' => 'Enable automatic cleanup of orphaned files' ] ]; $updatedSettings = []; $validationErrors = []; foreach ($input as $key => $value) { if (!array_key_exists($key, $allowedKeys)) { $validationErrors[] = "Unknown configuration key: $key"; continue; } $rules = $allowedKeys[$key]; // Validate based on type switch ($rules['type']) { case 'integer': if (!is_numeric($value)) { $validationErrors[] = "$key must be a number"; continue 2; } $value = (int) $value; if (isset($rules['min']) && $value < $rules['min']) { $validationErrors[] = "$key must be at least {$rules['min']}"; continue 2; } if (isset($rules['max']) && $value > $rules['max']) { $validationErrors[] = "$key must not exceed {$rules['max']}"; continue 2; } break; case 'string': if (!is_string($value)) { $validationErrors[] = "$key must be a string"; continue 2; } $value = trim($value); if (empty($value)) { $validationErrors[] = "$key cannot be empty"; continue 2; } if (isset($rules['max_length']) && strlen($value) > $rules['max_length']) { $validationErrors[] = "$key cannot exceed {$rules['max_length']} characters"; continue 2; } break; case 'boolean': $value = (bool) $value; break; case 'array': if (!is_array($value)) { $validationErrors[] = "$key must be an array"; continue 2; } if (isset($rules['allowed_values'])) { foreach ($value as $item) { if (!in_array($item, $rules['allowed_values'])) { $validationErrors[] = "$key contains invalid value: $item"; continue 3; } } } break; case 'url': if (!is_string($value)) { $validationErrors[] = "$key must be a string"; continue 2; } $value = trim($value); if (empty($value) || !filter_var($value, FILTER_VALIDATE_URL)) { $validationErrors[] = "$key must be a valid URL"; continue 2; } break; } // Additional validation for storage limits if ($key === 'storage_max_total_size') { $currentUsage = $this->getStorageUsageInfo(); if ($currentUsage && isset($currentUsage['current_usage']) && $value < $currentUsage['current_usage']) { $validationErrors[] = "storage_max_total_size cannot be less than current usage ({$this->formatBytes($currentUsage['current_usage'])})"; continue; } } // Update configuration $this->dataManager->setConfig($key, $value); $updatedSettings[$key] = $value; } if (!empty($validationErrors)) { return $this->error('Configuration validation failed', 400, 'VALIDATION_ERROR', [ 'validation_errors' => $validationErrors ]); } if (empty($updatedSettings)) { return $this->error('No valid configuration settings provided', 400, 'NO_VALID_SETTINGS'); } // Get updated storage info if storage settings were changed $storageInfo = null; if (array_intersect_key($updatedSettings, array_flip(['storage_max_total_size', 'storage_cleanup_enabled']))) { $storageInfo = $this->getStorageUsageInfo(); } $response = [ 'message' => 'Configuration updated successfully', 'updated_settings' => $updatedSettings, 'updated_count' => count($updatedSettings) ]; if ($storageInfo) { $response['storage_info'] = $storageInfo; } return $this->success($response); } catch (Exception $e) { return $this->error('Failed to update configuration: '.$e->getMessage()); } } /** * Get storage usage information and enforce limits * @return array Storage usage data */ private function getStorageUsageInfo() { try { $albumsPath = $this->albumsPath; $maxSize = $this->dataManager->getConfig('storage_max_total_size') ?? 1073741824; // 1GB default // Calculate current storage usage $currentUsage = 0; $fileCount = 0; $albumCount = 0; if (is_dir($albumsPath)) { $dirs = scandir($albumsPath); if ($dirs !== false) { foreach ($dirs as $dir) { if ($dir === '.' || $dir === '..') { continue; } $albumPath = $albumsPath.$dir; if (is_dir($albumPath)) { $albumCount++; $files = scandir($albumPath); if ($files !== false) { foreach ($files as $file) { if ($file === '.' || $file === '..') { continue; } $filePath = $albumPath.'/'.$file; if (is_file($filePath)) { $currentUsage += filesize($filePath); $fileCount++; } } } } } } } $usagePercentage = $maxSize > 0 ? round(($currentUsage / $maxSize) * 100, 2) : 0; $availableSpace = max(0, $maxSize - $currentUsage); return [ 'current_usage' => $currentUsage, 'current_usage_formatted' => $this->formatBytes($currentUsage), 'max_size' => $maxSize, 'max_size_formatted' => $this->formatBytes($maxSize), 'available_space' => $availableSpace, 'available_space_formatted' => $this->formatBytes($availableSpace), 'usage_percentage' => $usagePercentage, 'file_count' => $fileCount, 'album_count' => $albumCount, 'is_near_limit' => $usagePercentage >= 80, 'is_over_limit' => $usagePercentage >= 100, 'can_upload' => $usagePercentage < 95 // Leave 5% buffer ]; } catch (Exception $e) { error_log('Storage usage calculation error: '.$e->getMessage()); return [ 'current_usage' => 0, 'current_usage_formatted' => '0 B', 'max_size' => $maxSize ?? 1073741824, 'max_size_formatted' => $this->formatBytes($maxSize ?? 1073741824), 'available_space' => $maxSize ?? 1073741824, 'available_space_formatted' => $this->formatBytes($maxSize ?? 1073741824), 'usage_percentage' => 0, 'file_count' => 0, 'album_count' => 0, 'is_near_limit' => false, 'is_over_limit' => false, 'can_upload' => true, 'error' => 'Failed to calculate storage usage' ]; } } /** * Get storage statistics and usage information * @return mixed JSON response with storage statistics */ private function getStorageStats() { try { $stats = $this->dataManager->getStorageStats($this->albumsPath); // Format file sizes for display $stats['total_size_formatted'] = $this->formatBytes($stats['total_size']); $stats['max_storage_size_formatted'] = $this->formatBytes($stats['max_storage_size']); $stats['available_space_formatted'] = $this->formatBytes($stats['available_space']); // Format largest files foreach ($stats['largest_files'] as &$file) { $file['size_formatted'] = $this->formatBytes($file['size']); } // Format album sizes foreach ($stats['album_sizes'] as &$album) { $album['size_formatted'] = $this->formatBytes($album['size']); } // Format orphaned files foreach ($stats['orphaned_files'] as &$file) { $file['size_formatted'] = $this->formatBytes($file['size']); } // Format file types foreach ($stats['file_types'] as $ext => &$typeInfo) { $typeInfo['size_formatted'] = $this->formatBytes($typeInfo['size']); } return $this->success([ 'message' => 'Storage statistics retrieved successfully', 'stats' => $stats ]); } catch (Exception $e) { return $this->error('Failed to get storage statistics: '.$e->getMessage()); } } /** * Clean up orphaned files (files not referenced in database) * @return mixed JSON response with cleanup results */ private function cleanupOrphanedFiles() { try { // Get JSON input $input = json_decode(file_get_contents('php://input'), true); $dryRun = isset($input['dry_run']) ? (bool) $input['dry_run'] : false; $result = $this->dataManager->cleanupOrphanedFiles($this->albumsPath, $dryRun); // Format file sizes for display $result['space_freed_formatted'] = $this->formatBytes($result['space_freed']); foreach ($result['deleted_files'] as &$file) { $file['size_formatted'] = $this->formatBytes($file['size']); } foreach ($result['deleted_thumbnails'] as &$file) { $file['size_formatted'] = $this->formatBytes($file['size']); } $message = $dryRun ? 'Dry run completed - no files were actually deleted' : 'Orphaned files cleanup completed successfully'; return $this->success([ 'message' => $message, 'dry_run' => $dryRun, 'cleanup_result' => $result ]); } catch (Exception $e) { return $this->error('Failed to cleanup orphaned files: '.$e->getMessage()); } } /** * Generate missing thumbnails for photos * @return mixed JSON response with thumbnail generation results */ private function generateThumbnails() { try { // Get configuration for image processing $config = $this->dataManager->getAllConfig(); $imageProcessor = new ImageProcessor($config); $result = $this->dataManager->generateMissingThumbnails($this->albumsPath, $imageProcessor); // Format file sizes for display foreach ($result['generated_thumbnails'] as &$thumbnail) { $thumbnail['size_formatted'] = $this->formatBytes($thumbnail['size']); } return $this->success([ 'message' => 'Thumbnail generation completed successfully', 'generation_result' => $result ]); } catch (Exception $e) { return $this->error('Failed to generate thumbnails: '.$e->getMessage()); } } /** * Get storage usage by album * @return mixed JSON response with album storage usage */ private function getAlbumStorageUsage() { try { $usage = $this->dataManager->getAlbumStorageUsage($this->albumsPath); return $this->success([ 'message' => 'Album storage usage retrieved successfully', 'album_usage' => $usage ]); } catch (Exception $e) { return $this->error('Failed to get album storage usage: '.$e->getMessage()); } } /** * Send JSON response with proper HTTP status code * @param array $response Response data * @return string JSON response */ private function sendResponse($response) { // Set HTTP status code if specified if (isset($response['error']['http_status'])) { http_response_code($response['error']['http_status']); } elseif (isset($response['meta']['http_status'])) { http_response_code($response['meta']['http_status']); } return json_encode($response, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } /** * Trigger RSS feed auto-update mechanism * Called when photos are added or albums are modified * @return bool Success status */ private function triggerRSSAutoUpdate() { try { // Get base URL from configuration $config = $this->dataManager->getAllConfig(); $baseUrl = $config['rss_base_url'] ?? $this->getBaseUrl(); // Create RSS generator $rssGenerator = new RSSGenerator($this->dataManager, $baseUrl); // Trigger auto-update return $rssGenerator->triggerAutoUpdate(); } catch (Exception $e) { $this->errorHandler->logError( 'RSS auto-update failed: '.$e->getMessage(), 'RSS_AUTO_UPDATE_ERROR', ['exception' => get_class($e)], ['method' => 'triggerRSSAutoUpdate'] ); return false; } } /** * Get base URL from current request * @return string Base URL */ private function getBaseUrl() { $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://'; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; $path = dirname($_SERVER['SCRIPT_NAME']); return $protocol.$host.rtrim($path, '/').'/'; } /** * Check authentication status * @return string JSON response */ private function checkAuthStatus() { $userInfo = $this->auth->getCurrentUser(); $successResponse = $this->errorHandler->createSuccessResponse($userInfo); return $this->sendResponse($successResponse); } } // Handle the request $api = new PhotoGalleryAPI(); echo $api->handleRequest();