$maxSize) { $max = $fileType === 'photo' ? '20MB' : '500MB'; Security::abort(413, "Ukuran file melebihi batas ($max untuk $fileType)."); } // Cek error upload if ($file['error'] !== UPLOAD_ERR_OK) { Security::abort(500, 'Gagal mengupload file.'); } // Generate UUID & path $uuid = self::generateUUID(); $extension = self::getExtension($mimeType); $subDir = $fileType === 'photo' ? 'photos' : 'videos'; $yearMonth = date('Y/m'); $dir = STORAGE_BASE_PATH . "/$subDir/$yearMonth"; $storedName = "$uuid.$extension"; $filePath = "$dir/$storedName"; if (!is_dir($dir)) { mkdir($dir, 0755, true); } // Pindahkan file if (!move_uploaded_file($file['tmp_name'], $filePath)) { Security::abort(500, 'Gagal menyimpan file ke disk.'); } // Ambil metadata tambahan $metadata = []; $width = $height = $duration = null; if ($fileType === 'photo') { [$width, $height] = getimagesize($filePath) ?: [null, null]; } // Simpan ke database $db = Database::getConnection(); $db->prepare(" INSERT INTO storage_files (api_key_id, file_uuid, original_name, stored_name, file_type, mime_type, file_size, file_path, width, height, duration, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ")->execute([ $keyData['id'], $uuid, Security::sanitize($file['name']), $storedName, $fileType, $mimeType, $file['size'], "$subDir/$yearMonth/$storedName", $width, $height, $duration, $metadata ? json_encode($metadata) : null, ]); Security::respond([ 'file_uuid' => $uuid, 'original_name' => $file['name'], 'file_type' => $fileType, 'mime_type' => $mimeType, 'file_size' => $file['size'], 'file_size_hr' => self::formatBytes($file['size']), 'url' => STORAGE_BASE_URL . "/$subDir/$yearMonth/$storedName", 'width' => $width, 'height' => $height, ], 201, 'File berhasil diupload.'); } // ── GET /storage/files ──────────────────────────────────── public static function listFiles(): void { $keyData = Security::validateApiKey('storage'); $type = $_GET['type'] ?? null; // photo atau video $page = max(1, (int)($_GET['page'] ?? 1)); $perPage = min(100, (int)($_GET['per_page'] ?? 20)); $offset = ($page - 1) * $perPage; $db = Database::getConnection(); $where = 'WHERE api_key_id = ?'; $params = [$keyData['id']]; if ($type && in_array($type, ['photo', 'video'])) { $where .= ' AND file_type = ?'; $params[] = $type; } $total = $db->prepare("SELECT COUNT(*) FROM storage_files $where"); $total->execute($params); $totalCount = $total->fetchColumn(); $params[] = $perPage; $params[] = $offset; $stmt = $db->prepare(" SELECT file_uuid, original_name, file_type, mime_type, file_size, width, height, duration, created_at, CONCAT('" . STORAGE_BASE_URL . "/', file_path) AS url FROM storage_files $where ORDER BY created_at DESC LIMIT ? OFFSET ? "); $stmt->execute($params); $files = $stmt->fetchAll(); Security::respond([ 'files' => $files, 'total' => (int)$totalCount, 'page' => $page, 'per_page' => $perPage, 'total_pages' => ceil($totalCount / $perPage), ]); } // ── GET /storage/files/{uuid} ───────────────────────────── public static function getFile(string $uuid): void { $keyData = Security::validateApiKey('storage'); $db = Database::getConnection(); $stmt = $db->prepare(" SELECT *, CONCAT('" . STORAGE_BASE_URL . "/', file_path) AS url FROM storage_files WHERE file_uuid = ? AND api_key_id = ? LIMIT 1 "); $stmt->execute([$uuid, $keyData['id']]); $file = $stmt->fetch(); if (!$file) Security::abort(404, 'File tidak ditemukan.'); // Parse metadata jika ada if ($file['metadata']) { $file['metadata'] = json_decode($file['metadata'], true); } Security::respond($file); } // ── DELETE /storage/files/{uuid} ────────────────────────── public static function deleteFile(string $uuid): void { $keyData = Security::validateApiKey('storage'); $db = Database::getConnection(); $stmt = $db->prepare("SELECT * FROM storage_files WHERE file_uuid = ? AND api_key_id = ? LIMIT 1"); $stmt->execute([$uuid, $keyData['id']]); $file = $stmt->fetch(); if (!$file) Security::abort(404, 'File tidak ditemukan.'); // Hapus dari disk $fullPath = STORAGE_BASE_PATH . '/' . $file['file_path']; if (file_exists($fullPath)) { unlink($fullPath); } // Hapus dari DB $db->prepare("DELETE FROM storage_files WHERE id = ?")->execute([$file['id']]); Security::respond(['deleted' => true, 'file_uuid' => $uuid], 200, 'File berhasil dihapus.'); } // ── GET /storage/stats ──────────────────────────────────── public static function stats(): void { $keyData = Security::validateApiKey('storage'); $db = Database::getConnection(); $stmt = $db->prepare(" SELECT COUNT(*) AS total_files, SUM(file_size) AS total_size, SUM(CASE WHEN file_type = 'photo' THEN 1 ELSE 0 END) AS total_photos, SUM(CASE WHEN file_type = 'video' THEN 1 ELSE 0 END) AS total_videos FROM storage_files WHERE api_key_id = ? "); $stmt->execute([$keyData['id']]); $stats = $stmt->fetch(); $stats['total_size_hr'] = self::formatBytes($stats['total_size'] ?? 0); Security::respond($stats); } // ─── HELPERS ───────────────────────────────────────────── private static function detectFileType(string $mime): ?string { if (in_array($mime, ALLOWED_PHOTO_TYPES)) return 'photo'; if (in_array($mime, ALLOWED_VIDEO_TYPES)) return 'video'; return null; } private static function getExtension(string $mime): string { $map = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'video/mp4' => 'mp4', 'video/webm' => 'webm', 'video/quicktime'=> 'mov', 'video/avi' => 'avi', ]; return $map[$mime] ?? 'bin'; } private static function generateUUID(): string { $data = random_bytes(16); $data[6] = chr(ord($data[6]) & 0x0f | 0x40); $data[8] = chr(ord($data[8]) & 0x3f | 0x80); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } private static function formatBytes(int $bytes): string { if ($bytes >= 1073741824) return number_format($bytes / 1073741824, 2) . ' GB'; if ($bytes >= 1048576) return number_format($bytes / 1048576, 2) . ' MB'; if ($bytes >= 1024) return number_format($bytes / 1024, 2) . ' KB'; return $bytes . ' B'; } }