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>
124 lines
4.5 KiB
PHP
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']),
|
|
],
|
|
]);
|