<?php
/**
* DataManager class for handling JSON file-based data storage
* Provides CRUD operations for albums, tags, photos, and configuration
*/
class DataManager
{
/**
* @var mixed
*/
private $dataDir;
/**
* Constructor - Initialize data directory
* @param string $dataDir Path to data directory
*/
public function __construct($dataDir = 'data')
{
$this->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());
}
}
}