dataDir = rtrim($dataDir, '/'); $this->ensureDataDirectory(); } /** * Ensure data directory exists */ private function ensureDataDirectory() { if (!is_dir($this->dataDir)) { if (!mkdir($this->dataDir, 0755, true)) { throw new Exception('Failed to create data directory: '.$this->dataDir); } } } /** * Initialize data files with default values */ public function initializeData() { try { // Initialize albums.json if (!file_exists($this->dataDir.'/albums.json')) { $this->saveJsonFile('albums.json', []); } // Initialize tags.json if (!file_exists($this->dataDir.'/tags.json')) { $this->saveJsonFile('tags.json', []); } // Initialize counters.json if (!file_exists($this->dataDir.'/counters.json')) { $this->saveJsonFile('counters.json', [ 'next_album_id' => 1, 'next_photo_id' => 1, 'next_tag_id' => 1 ]); } // Initialize config.json with default values if (!file_exists($this->dataDir.'/config.json')) { $this->setDefaultConfig(); } return true; } catch (Exception $e) { throw new Exception('Data initialization failed: '.$e->getMessage()); } } /** * Set default configuration values */ private function setDefaultConfig() { $defaultConfig = [ 'upload_max_file_size' => 10485760, // 10MB 'upload_allowed_types' => ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff'], 'image_webp_quality' => 80, 'image_max_width' => 1000, 'image_max_height' => 1000, 'image_thumbnail_size' => 300, 'rss_title' => 'Photo Gallery RSS Feed', 'rss_description' => 'Latest photos from selected albums', 'rss_max_items' => 50, 'rss_base_url' => 'http://localhost/', 'storage_max_total_size' => 1073741824, // 1GB 'storage_cleanup_enabled' => true ]; $this->saveJsonFile('config.json', $defaultConfig); } // ALBUM OPERATIONS /** * Get all albums with optional metadata * @param bool $includeMetadata Include tags and photo count * @return array Array of albums */ public function getAlbums($includeMetadata = true) { try { $albums = $this->loadJsonFile('albums.json'); if ($includeMetadata) { foreach ($albums as &$album) { $album['photo_count'] = count($album['photos'] ?? []); } } // Sort by created_at descending uasort($albums, function ($a, $b) { return strtotime($b['created_at']) - strtotime($a['created_at']); }); return array_values($albums); } catch (Exception $e) { throw new Exception('Failed to get albums: '.$e->getMessage()); } } /** * Get single album by ID * @param int $id Album ID * @return array|null Album data or null if not found */ public function getAlbum($id) { try { $albums = $this->loadJsonFile('albums.json'); // Convert to integer for consistency $id = intval($id); if (isset($albums[$id])) { $album = $albums[$id]; $album['photo_count'] = count($album['photos'] ?? []); return $album; } return null; } catch (Exception $e) { throw new Exception('Failed to get album: '.$e->getMessage()); } } /** * Create new album * @param string $title Album title * @param string $description Album description * @return int Created album ID */ public function createAlbum($title, $description = '') { try { // Validate title $title = trim($title); if (empty($title)) { throw new Exception('Album title is required and cannot be empty'); } $albums = $this->loadJsonFile('albums.json'); // Generate unique ID using timestamp + microseconds $id = time(); $album = [ 'id' => $id, 'title' => $title, 'description' => $description, 'created_at' => date('c'), 'updated_at' => date('c'), 'photo_count' => 0, 'is_rss_enabled' => false, 'tags' => [], 'photos' => [] ]; $albums[$id] = $album; $this->saveJsonFile('albums.json', $albums); return $id; } catch (Exception $e) { throw new Exception('Failed to create album: '.$e->getMessage()); } } /** * Update album * @param int $id Album ID * @param array $data Album data to update * @return bool Success status */ public function updateAlbum($id, $data) { try { $albums = $this->loadJsonFile('albums.json'); // Convert to integer for consistency $id = intval($id); if (!isset($albums[$id])) { return false; } $allowedFields = ['title', 'description', 'is_rss_enabled']; foreach ($data as $field => $value) { if (in_array($field, $allowedFields)) { $albums[$id][$field] = $value; } } $albums[$id]['updated_at'] = date('c'); $this->saveJsonFile('albums.json', $albums); return true; } catch (Exception $e) { throw new Exception('Failed to update album: '.$e->getMessage()); } } /** * Delete album and associated data * @param int $id Album ID * @return bool Success status */ public function deleteAlbum($id) { try { $albums = $this->loadJsonFile('albums.json'); // Convert to integer for consistency $id = intval($id); if (!isset($albums[$id])) { return false; } unset($albums[$id]); $this->saveJsonFile('albums.json', $albums); return true; } catch (Exception $e) { throw new Exception('Failed to delete album: '.$e->getMessage()); } } // TAG OPERATIONS /** * Get all tags * @return array Array of tags */ public function getTags() { try { $tags = $this->loadJsonFile('tags.json'); $albums = $this->loadJsonFile('albums.json'); // Calculate album count for each tag foreach ($tags as &$tag) { $tag['album_count'] = 0; foreach ($albums as $album) { if (in_array($tag['id'], $album['tags'] ?? [])) { $tag['album_count']++; } } } // Sort by name uasort($tags, function ($a, $b) { return strcmp($a['name'], $b['name']); }); return array_values($tags); } catch (Exception $e) { throw new Exception('Failed to get tags: '.$e->getMessage()); } } /** * Create new tag * @param string $name Tag name * @param string $color Tag color (hex) * @return int Created tag ID */ public function createTag($name, $color = '#3498db') { try { $tags = $this->loadJsonFile('tags.json'); // Check if tag name already exists foreach ($tags as $tag) { if (strtolower($tag['name']) === strtolower($name)) { throw new Exception('Tag name already exists'); } } $id = time(); $tag = [ 'id' => $id, 'name' => $name, 'color' => $color ]; $tags[$id] = $tag; $this->saveJsonFile('tags.json', $tags); return $id; } catch (Exception $e) { throw new Exception('Failed to create tag: '.$e->getMessage()); } } /** * Get tags for specific album * @param int $albumId Album ID * @return array Array of tags */ public function getAlbumTags($albumId) { try { $album = $this->getAlbum($albumId); if (!$album) { return []; } $tags = $this->loadJsonFile('tags.json'); $albumTags = []; foreach ($album['tags'] ?? [] as $tagId) { if (isset($tags[$tagId])) { $albumTags[] = $tags[$tagId]; } } return $albumTags; } catch (Exception $e) { throw new Exception('Failed to get album tags: '.$e->getMessage()); } } /** * Update album tags * @param int $albumId Album ID * @param array $tagIds Array of tag IDs * @return bool Success status */ public function updateAlbumTags($albumId, $tagIds) { try { $albums = $this->loadJsonFile('albums.json'); // Convert to integer for consistency $albumId = intval($albumId); if (!isset($albums[$albumId])) { return false; } // Validate all tag IDs exist $allTags = $this->loadJsonFile('tags.json'); $validTagIds = array_column($allTags, 'id'); // ใช้ array_column แทน array_keys foreach ($tagIds as $tagId) { if (!in_array((int) $tagId, $validTagIds)) { // แปลงเป็น int สำหรับเปรียบเทียบ throw new Exception('Invalid tag ID: '.$tagId); } } $albums[$albumId]['tags'] = array_map('intval', $tagIds); $albums[$albumId]['updated_at'] = date('c'); $this->saveJsonFile('albums.json', $albums); return true; } catch (Exception $e) { throw new Exception('Failed to update album tags: '.$e->getMessage()); } } // PHOTO OPERATIONS /** * Get photos for album * @param int $albumId Album ID * @return array Array of photos */ public function getPhotos($albumId) { try { $album = $this->getAlbum($albumId); if (!$album) { return []; } $photos = $album['photos'] ?? []; // Sort by uploaded_at descending usort($photos, function ($a, $b) { return strtotime($b['uploaded_at']) - strtotime($a['uploaded_at']); }); return $photos; } catch (Exception $e) { throw new Exception('Failed to get photos: '.$e->getMessage()); } } /** * Add photo to database * @param int $albumId Album ID * @param array $photoData Photo metadata * @return int Created photo ID */ public function addPhoto($albumId, $photoData) { try { $albums = $this->loadJsonFile('albums.json'); if (!isset($albums[$albumId])) { throw new Exception('Album not found'); } $photoId = time(); $photo = [ 'id' => $photoId, 'filename' => $photoData['filename'], 'original_filename' => $photoData['original_filename'] ?? null, 'file_size' => $photoData['file_size'] ?? null, 'width' => $photoData['width'] ?? null, 'height' => $photoData['height'] ?? null, 'uploaded_at' => date('c') ]; if (!isset($albums[$albumId]['photos'])) { $albums[$albumId]['photos'] = []; } $albums[$albumId]['photos'][] = $photo; $albums[$albumId]['photo_count'] = count($albums[$albumId]['photos']); $albums[$albumId]['updated_at'] = date('c'); $this->saveJsonFile('albums.json', $albums); return $photoId; } catch (Exception $e) { throw new Exception('Failed to add photo: '.$e->getMessage()); } } /** * Delete photo * @param int $id Photo ID * @return bool Success status */ public function deletePhoto($id) { try { $albums = $this->loadJsonFile('albums.json'); foreach ($albums as $albumId => &$album) { if (isset($album['photos'])) { foreach ($album['photos'] as $index => $photo) { if ($photo['id'] == $id) { unset($album['photos'][$index]); $album['photos'] = array_values($album['photos']); // Re-index array $album['photo_count'] = count($album['photos']); $album['updated_at'] = date('c'); $this->saveJsonFile('albums.json', $albums); return true; } } } } return false; } catch (Exception $e) { throw new Exception('Failed to delete photo: '.$e->getMessage()); } } /** * Delete photo from album by filename * @param int $albumId Album ID * @param string $filename Photo filename * @return bool Success status */ public function deletePhotoByFilename($albumId, $filename) { try { $albums = $this->loadJsonFile('albums.json'); if (!isset($albums[$albumId])) { return false; } $album = &$albums[$albumId]; if (isset($album['photos'])) { foreach ($album['photos'] as $index => $photo) { if ($photo['filename'] === $filename) { unset($album['photos'][$index]); $album['photos'] = array_values($album['photos']); // Re-index array $album['photo_count'] = count($album['photos']); $album['updated_at'] = date('c'); $this->saveJsonFile('albums.json', $albums); return true; } } } return false; } catch (Exception $e) { throw new Exception('Failed to delete photo by filename: '.$e->getMessage()); } } // CONFIGURATION OPERATIONS /** * Get configuration value * @param string $key Configuration key * @return mixed Configuration value or null if not found */ public function getConfig($key) { try { $config = $this->loadJsonFile('config.json'); return isset($config[$key]) ? $config[$key] : null; } catch (Exception $e) { throw new Exception('Failed to get config: '.$e->getMessage()); } } /** * Set configuration value * @param string $key Configuration key * @param mixed $value Configuration value * @return bool Success status */ public function setConfig($key, $value) { try { $config = $this->loadJsonFile('config.json'); $config[$key] = $value; $this->saveJsonFile('config.json', $config); return true; } catch (Exception $e) { throw new Exception('Failed to set config: '.$e->getMessage()); } } /** * Get all configuration values * @return array Associative array of configuration */ public function getAllConfig() { try { return $this->loadJsonFile('config.json'); } catch (Exception $e) { throw new Exception('Failed to get all config: '.$e->getMessage()); } } /** * Update multiple configuration values at once * @param array $configData Associative array of configuration keys and values * @return bool Success status */ public function updateConfig($configData) { try { $config = $this->loadJsonFile('config.json'); foreach ($configData as $key => $value) { $config[$key] = $value; } $this->saveJsonFile('config.json', $config); return true; } catch (Exception $e) { throw new Exception('Failed to update config: '.$e->getMessage()); } } /** * Reset configuration to default values * @return bool Success status */ public function resetConfig() { try { $this->setDefaultConfig(); return true; } catch (Exception $e) { throw new Exception('Failed to reset config: '.$e->getMessage()); } } /** * Get configuration schema with validation rules * @return array Configuration schema */ public function getConfigSchema() { return [ 'upload_max_file_size' => [ 'type' => 'integer', 'min' => 1048576, // 1MB 'max' => 104857600, // 100MB 'default' => 10485760, // 10MB 'description' => 'Maximum file size for uploads in bytes' ], 'upload_allowed_types' => [ 'type' => 'array', 'allowed_values' => ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff', 'image/webp'], 'default' => ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff'], 'description' => 'Allowed MIME types for file uploads' ], 'image_webp_quality' => [ 'type' => 'integer', 'min' => 1, 'max' => 100, 'default' => 80, 'description' => 'WebP compression quality (1-100, higher is better quality)' ], 'image_max_width' => [ 'type' => 'integer', 'min' => 100, 'max' => 5000, 'default' => 1000, 'description' => 'Maximum image width in pixels' ], 'image_max_height' => [ 'type' => 'integer', 'min' => 100, 'max' => 5000, 'default' => 1000, 'description' => 'Maximum image height in pixels' ], 'image_thumbnail_size' => [ 'type' => 'integer', 'min' => 50, 'max' => 500, 'default' => 300, 'description' => 'Thumbnail size in pixels (square)' ], 'rss_title' => [ 'type' => 'string', 'max_length' => 255, 'default' => 'Photo Gallery RSS Feed', 'description' => 'Title for the RSS feed' ], 'rss_description' => [ 'type' => 'string', 'max_length' => 1000, 'default' => 'Latest photos from selected albums', 'description' => 'Description for the RSS feed' ], 'rss_max_items' => [ 'type' => 'integer', 'min' => 1, 'max' => 200, 'default' => 50, 'description' => 'Maximum number of items in RSS feed' ], 'rss_base_url' => [ 'type' => 'url', 'default' => 'http://localhost/', 'description' => 'Base URL for RSS feed links' ], 'storage_max_total_size' => [ 'type' => 'integer', 'min' => 104857600, // 100MB 'max' => 10737418240, // 10GB 'default' => 1073741824, // 1GB 'description' => 'Maximum total storage size in bytes' ], 'storage_cleanup_enabled' => [ 'type' => 'boolean', 'default' => true, 'description' => 'Enable automatic cleanup of orphaned files' ] ]; } // STORAGE MANAGEMENT OPERATIONS /** * Get storage usage statistics * @param string $albumsPath Path to albums directory * @return array Storage statistics */ public function getStorageStats($albumsPath = 'albums') { try { $stats = [ 'total_size' => 0, 'total_files' => 0, 'total_thumbnails' => 0, 'albums_count' => 0, 'orphaned_files' => [], 'largest_files' => [], 'album_sizes' => [], 'file_types' => [], 'max_storage_size' => $this->getConfig('storage_max_total_size') ?? 1073741824, 'usage_percentage' => 0, 'available_space' => 0 ]; if (!is_dir($albumsPath)) { return $stats; } $albums = $this->loadJsonFile('albums.json'); $registeredFiles = []; // Collect all registered files from database foreach ($albums as $albumId => $album) { if (isset($album['photos'])) { foreach ($album['photos'] as $photo) { $registeredFiles[$albumId][] = $photo['filename']; if (isset($photo['file_size'])) { $stats['total_size'] += $photo['file_size']; } } } } // Scan filesystem for actual files $dirs = scandir($albumsPath); if ($dirs !== false) { foreach ($dirs as $dir) { if ($dir === '.' || $dir === '..') { continue; } $albumPath = $albumsPath.'/'.$dir; if (!is_dir($albumPath)) { continue; } $stats['albums_count']++; $albumSize = 0; $albumFiles = 0; $albumThumbnails = 0; $files = scandir($albumPath); if ($files !== false) { foreach ($files as $file) { if ($file === '.' || $file === '..') { continue; } $filePath = $albumPath.'/'.$file; if (!is_file($filePath)) { continue; } $fileSize = filesize($filePath); $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION)); // Track file types if (!isset($stats['file_types'][$fileExt])) { $stats['file_types'][$fileExt] = ['count' => 0, 'size' => 0]; } $stats['file_types'][$fileExt]['count']++; $stats['file_types'][$fileExt]['size'] += $fileSize; // Check if it's a thumbnail if (strpos($file, 'thumb_') === 0) { $stats['total_thumbnails']++; $albumThumbnails++; } else { $stats['total_files']++; $albumFiles++; // Add to largest files list $stats['largest_files'][] = [ 'filename' => $file, 'album_id' => $dir, 'size' => $fileSize, 'path' => $filePath ]; // Check if file is orphaned (not in database) if (!isset($registeredFiles[$dir]) || !in_array($file, $registeredFiles[$dir])) { $stats['orphaned_files'][] = [ 'filename' => $file, 'album_id' => $dir, 'size' => $fileSize, 'path' => $filePath, 'modified_at' => date('c', filemtime($filePath)) ]; } } $albumSize += $fileSize; } } $stats['album_sizes'][] = [ 'album_id' => $dir, 'size' => $albumSize, 'files' => $albumFiles, 'thumbnails' => $albumThumbnails ]; } } // Sort largest files by size (descending) usort($stats['largest_files'], function ($a, $b) { return $b['size'] - $a['size']; }); $stats['largest_files'] = array_slice($stats['largest_files'], 0, 20); // Top 20 // Sort album sizes by size (descending) usort($stats['album_sizes'], function ($a, $b) { return $b['size'] - $a['size']; }); // Calculate usage percentage and available space if ($stats['max_storage_size'] > 0) { $stats['usage_percentage'] = round(($stats['total_size'] / $stats['max_storage_size']) * 100, 2); $stats['available_space'] = $stats['max_storage_size'] - $stats['total_size']; } return $stats; } catch (Exception $e) { throw new Exception('Failed to get storage stats: '.$e->getMessage()); } } /** * Clean up orphaned files (files not referenced in database) * @param string $albumsPath Path to albums directory * @param bool $dryRun If true, only return what would be deleted without actually deleting * @return array Cleanup results */ public function cleanupOrphanedFiles($albumsPath = 'albums', $dryRun = false) { try { $result = [ 'deleted_files' => [], 'deleted_thumbnails' => [], 'total_deleted' => 0, 'space_freed' => 0, 'errors' => [] ]; if (!$this->getConfig('storage_cleanup_enabled')) { throw new Exception('Storage cleanup is disabled in configuration'); } $stats = $this->getStorageStats($albumsPath); $orphanedFiles = $stats['orphaned_files']; foreach ($orphanedFiles as $orphan) { try { if ($dryRun) { $result['deleted_files'][] = $orphan; $result['space_freed'] += $orphan['size']; } else { if (unlink($orphan['path'])) { $result['deleted_files'][] = $orphan; $result['space_freed'] += $orphan['size']; // Also try to delete corresponding thumbnail $thumbnailPath = dirname($orphan['path']).'/thumb_'.$orphan['filename']; if (file_exists($thumbnailPath)) { if (unlink($thumbnailPath)) { $result['deleted_thumbnails'][] = [ 'filename' => 'thumb_'.$orphan['filename'], 'album_id' => $orphan['album_id'], 'size' => filesize($thumbnailPath), 'path' => $thumbnailPath ]; } } } else { $result['errors'][] = 'Failed to delete: '.$orphan['path']; } } } catch (Exception $e) { $result['errors'][] = 'Error deleting '.$orphan['path'].': '.$e->getMessage(); } } $result['total_deleted'] = count($result['deleted_files']) + count($result['deleted_thumbnails']); return $result; } catch (Exception $e) { throw new Exception('Failed to cleanup orphaned files: '.$e->getMessage()); } } /** * Generate thumbnails for photos that don't have them * @param string $albumsPath Path to albums directory * @param ImageProcessor $imageProcessor Image processor instance * @return array Generation results */ public function generateMissingThumbnails($albumsPath = 'albums', $imageProcessor = null) { try { $result = [ 'generated_thumbnails' => [], 'skipped_files' => [], 'errors' => [], 'total_generated' => 0 ]; if (!$imageProcessor) { throw new Exception('ImageProcessor instance required for thumbnail generation'); } $albums = $this->loadJsonFile('albums.json'); foreach ($albums as $albumId => $album) { if (!isset($album['photos'])) { continue; } $albumPath = $albumsPath.'/'.$albumId; if (!is_dir($albumPath)) { continue; } foreach ($album['photos'] as $photo) { $imagePath = $albumPath.'/'.$photo['filename']; $thumbnailPath = $albumPath.'/thumb_'.$photo['filename']; // Skip if thumbnail already exists if (file_exists($thumbnailPath)) { $result['skipped_files'][] = [ 'filename' => $photo['filename'], 'album_id' => $albumId, 'reason' => 'Thumbnail already exists' ]; continue; } // Skip if original image doesn't exist if (!file_exists($imagePath)) { $result['skipped_files'][] = [ 'filename' => $photo['filename'], 'album_id' => $albumId, 'reason' => 'Original image not found' ]; continue; } try { $thumbnailSize = $this->getConfig('image_thumbnail_size') ?? 300; if ($imageProcessor->generateThumbnail($imagePath, $thumbnailPath, $thumbnailSize)) { $result['generated_thumbnails'][] = [ 'filename' => $photo['filename'], 'album_id' => $albumId, 'thumbnail_path' => $thumbnailPath, 'size' => filesize($thumbnailPath) ]; $result['total_generated']++; } else { $result['errors'][] = 'Failed to generate thumbnail for: '.$imagePath; } } catch (Exception $e) { $result['errors'][] = 'Error generating thumbnail for '.$imagePath.': '.$e->getMessage(); } } } return $result; } catch (Exception $e) { throw new Exception('Failed to generate missing thumbnails: '.$e->getMessage()); } } /** * Check if storage limit would be exceeded by adding specified size * @param int $additionalSize Size to be added in bytes * @param string $albumsPath Path to albums directory * @return array Storage check result */ public function checkStorageLimit($additionalSize, $albumsPath = 'albums') { try { $stats = $this->getStorageStats($albumsPath); $maxSize = $stats['max_storage_size']; $currentSize = $stats['total_size']; $newTotalSize = $currentSize + $additionalSize; return [ 'can_upload' => $newTotalSize <= $maxSize, 'current_usage' => $currentSize, 'max_size' => $maxSize, 'additional_size' => $additionalSize, 'new_total_size' => $newTotalSize, 'available_space' => $maxSize - $currentSize, 'usage_percentage' => $maxSize > 0 ? round(($currentSize / $maxSize) * 100, 2) : 0, 'new_usage_percentage' => $maxSize > 0 ? round(($newTotalSize / $maxSize) * 100, 2) : 0 ]; } catch (Exception $e) { throw new Exception('Failed to check storage limit: '.$e->getMessage()); } } /** * Update file size tracking for a photo * @param int $photoId Photo ID * @param int $fileSize File size in bytes * @return bool Success status */ public function updatePhotoFileSize($photoId, $fileSize) { try { $albums = $this->loadJsonFile('albums.json'); foreach ($albums as $albumId => &$album) { if (isset($album['photos'])) { foreach ($album['photos'] as &$photo) { if ($photo['id'] == $photoId) { $photo['file_size'] = $fileSize; $album['updated_at'] = date('c'); $this->saveJsonFile('albums.json', $albums); return true; } } } } return false; } catch (Exception $e) { throw new Exception('Failed to update photo file size: '.$e->getMessage()); } } /** * Get storage usage by album * @param string $albumsPath Path to albums directory * @return array Album storage usage */ public function getAlbumStorageUsage($albumsPath = 'albums') { try { $stats = $this->getStorageStats($albumsPath); $albums = $this->loadJsonFile('albums.json'); $usage = []; foreach ($stats['album_sizes'] as $albumStat) { $albumId = $albumStat['album_id']; $albumData = isset($albums[$albumId]) ? $albums[$albumId] : null; $usage[] = [ 'album_id' => $albumId, 'title' => $albumData['title'] ?? "Album $albumId", 'size' => $albumStat['size'], 'size_formatted' => $this->formatBytes($albumStat['size']), 'files' => $albumStat['files'], 'thumbnails' => $albumStat['thumbnails'], 'photo_count' => $albumData['photo_count'] ?? 0, 'created_at' => $albumData['created_at'] ?? null, 'updated_at' => $albumData['updated_at'] ?? null ]; } return $usage; } catch (Exception $e) { throw new Exception('Failed to get album storage usage: '.$e->getMessage()); } } /** * Format bytes to human readable format * @param int $bytes Size in bytes * @param int $precision Decimal precision * @return string Formatted size */ private function formatBytes($bytes, $precision = 2) { $units = ['B', 'KB', 'MB', 'GB', 'TB']; for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) { $bytes /= 1024; } return round($bytes, $precision).' '.$units[$i]; } /** * Remove configuration key * @param string $key Configuration key to remove * @return bool Success status */ public function removeConfig($key) { try { $config = $this->loadJsonFile('config.json'); if (isset($config[$key])) { unset($config[$key]); $this->saveJsonFile('config.json', $config); return true; } return false; } catch (Exception $e) { throw new Exception('Failed to remove config: '.$e->getMessage()); } } // UTILITY METHODS /** * Load JSON file * @param string $filename Filename relative to data directory * @return array Decoded JSON data */ private function loadJsonFile($filename) { $filepath = $this->dataDir.'/'.$filename; if (!file_exists($filepath)) { return []; } $content = file_get_contents($filepath); if ($content === false) { throw new Exception('Failed to read file: '.$filepath); } $data = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('Invalid JSON in file '.$filename.': '.json_last_error_msg()); } return $data; } /** * Save data to JSON file * @param string $filename Filename relative to data directory * @param array $data Data to save * @return bool Success status */ private function saveJsonFile($filename, $data) { $filepath = $this->dataDir.'/'.$filename; $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); if ($json === false) { throw new Exception('Failed to encode JSON for file: '.$filename); } $result = file_put_contents($filepath, $json, LOCK_EX); if ($result === false) { throw new Exception('Failed to write file: '.$filepath); } return true; } /** * Get a single tag by ID * @param int $tagId Tag ID * @return array|null Tag data or null if not found */ public function getTag($tagId) { try { $tags = $this->loadJsonFile('tags.json'); // Convert to integer for consistency $tagId = intval($tagId); foreach ($tags as $tag) { if ($tag['id'] === $tagId) { return $tag; } } return null; } catch (Exception $e) { throw new Exception('Failed to get tag: '.$e->getMessage()); } } /** * Delete a tag by ID * @param int $tagId Tag ID * @return bool True if deletion was successful, false otherwise */ public function deleteTag($tagId) { try { $tags = $this->loadJsonFile('tags.json'); // Filter out the tag with the specified ID $updatedTags = array_filter($tags, function ($tag) use ($tagId) { return $tag['id'] !== intval($tagId); }); // Check if any tag was removed if (count($tags) === count($updatedTags)) { return false; // No tag was deleted } // Save the updated tags back to the file $this->saveJsonFile('tags.json', array_values($updatedTags)); return true; } catch (Exception $e) { throw new Exception('Failed to delete tag: '.$e->getMessage()); } } }