false, 'ERROR' => 'method_not_allowed'], 405); } $channelId = (int) ($_POST['ChannelID'] ?? 0); $uploaderAddress = trim($_POST['UploaderAddress'] ?? ''); $messageId = isset($_POST['MessageID']) ? (int) $_POST['MessageID'] : null; if ($channelId <= 0) jsonResponse(['OK' => false, 'ERROR' => 'channel_id_required']); if ($uploaderAddress === '') jsonResponse(['OK' => false, 'ERROR' => 'uploader_address_required']); // Verify channel exists $channel = queryOne("SELECT ID FROM Hub_Channels WHERE ID = ?", [$channelId]); if (!$channel) jsonResponse(['OK' => false, 'ERROR' => 'channel_not_found']); // Check file upload if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { $errCode = $_FILES['file']['error'] ?? -1; jsonResponse(['OK' => false, 'ERROR' => 'file_upload_failed', 'Code' => $errCode]); } $file = $_FILES['file']; $maxSize = 50 * 1024 * 1024; // 50MB if ($file['size'] > $maxSize) { jsonResponse(['OK' => false, 'ERROR' => 'file_too_large', 'MaxBytes' => $maxSize]); } // Sanitize filename $originalName = basename($file['name']); $originalName = preg_replace('/[^a-zA-Z0-9._\-]/', '_', $originalName); if ($originalName === '' || $originalName === '.') $originalName = 'upload'; $mimeType = $file['type'] ?: 'application/octet-stream'; // Storage path: /uploads/hub/{channelId}/{uuid}_{filename} $uuid = generateUUID(); $storageDir = uploadsRoot() . '/hub/' . $channelId; if (!is_dir($storageDir)) { mkdir($storageDir, 0755, true); } $storageName = $uuid . '_' . $originalName; $storagePath = $storageDir . '/' . $storageName; $relPath = 'uploads/hub/' . $channelId . '/' . $storageName; if (!move_uploaded_file($file['tmp_name'], $storagePath)) { jsonResponse(['OK' => false, 'ERROR' => 'file_save_failed']); } // Generate thumbnail for images $thumbnailRelPath = null; if (str_starts_with($mimeType, 'image/') && extension_loaded('gd')) { $thumbDir = $storageDir . '/thumbs'; if (!is_dir($thumbDir)) mkdir($thumbDir, 0755, true); $thumbName = $uuid . '_thumb.jpg'; $thumbPath = $thumbDir . '/' . $thumbName; $src = null; if ($mimeType === 'image/jpeg') $src = @imagecreatefromjpeg($storagePath); elseif ($mimeType === 'image/png') $src = @imagecreatefrompng($storagePath); elseif ($mimeType === 'image/gif') $src = @imagecreatefromgif($storagePath); elseif ($mimeType === 'image/webp') $src = @imagecreatefromwebp($storagePath); if ($src) { $origW = imagesx($src); $origH = imagesy($src); $maxThumb = 200; $ratio = min($maxThumb / $origW, $maxThumb / $origH, 1); $newW = (int) ($origW * $ratio); $newH = (int) ($origH * $ratio); $thumb = imagecreatetruecolor($newW, $newH); imagecopyresampled($thumb, $src, 0, 0, 0, 0, $newW, $newH, $origW, $origH); imagejpeg($thumb, $thumbPath, 80); imagedestroy($src); imagedestroy($thumb); $thumbnailRelPath = 'uploads/hub/' . $channelId . '/thumbs/' . $thumbName; } } // Insert to DB queryTimed( "INSERT INTO Hub_Files (MessageID, ChannelID, UploaderAddress, FileName, FileSize, MimeType, StoragePath, ThumbnailPath) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [$messageId, $channelId, $uploaderAddress, $originalName, $file['size'], $mimeType, $relPath, $thumbnailRelPath] ); $fileId = (int) lastInsertId(); $record = queryOne("SELECT * FROM Hub_Files WHERE ID = ?", [$fileId]); jsonResponse([ 'OK' => true, 'File' => [ 'ID' => (int) $record['ID'], 'MessageID' => $record['MessageID'] ? (int) $record['MessageID'] : null, 'ChannelID' => (int) $record['ChannelID'], 'UploaderAddress' => $record['UploaderAddress'], 'FileName' => $record['FileName'], 'FileSize' => (int) $record['FileSize'], 'MimeType' => $record['MimeType'], 'DownloadURL' => baseUrl() . '/' . $record['StoragePath'], 'ThumbnailURL' => $record['ThumbnailPath'] ? baseUrl() . '/' . $record['ThumbnailPath'] : null, 'CreatedAt' => toISO8601($record['CreatedAt']), ], ]);