payfrit-api/api/hub/files/upload.php
Mike 1dacefcf70 Add Hub Messages, Files, Users, Reactions, and Pins APIs
Complete backend for SprintChat Hub migration:
- Messages: send, edit, delete, list (paginated cursor), thread, search
- Files: upload (multipart), download, thumbnail, info, list
- Users: get, getByIds, search, status (online detection)
- Reactions: add, remove, list (grouped by emoji)
- Pins: pin, unpin, list (with message content)
- Channel stats: member/message/pinned/unread counts

4 new DB tables: Hub_Messages, Hub_Files, Hub_Reactions, Hub_PinnedPosts
21 new endpoints added to PUBLIC_ROUTES

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:03:14 +00:00

124 lines
4.5 KiB
PHP

<?php
/**
* POST /api/hub/files/upload.php
*
* Upload a file (multipart/form-data).
*
* Form fields:
* ChannelID int REQUIRED
* UploaderAddress string REQUIRED agent address
* MessageID int optional attach to an existing message
* file file REQUIRED the uploaded file
*
* Response: { OK: true, File: { ... } }
*/
require_once __DIR__ . '/../../helpers.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['OK' => 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']),
],
]);