feat: system tools and admin enhancements
This commit is contained in:
@@ -5,7 +5,8 @@ namespace App\Http\Controllers;
|
||||
use App\Models\Attachment;
|
||||
use App\Models\AttachmentExtension;
|
||||
use App\Models\Post;
|
||||
use App\Models\Setting;
|
||||
use App\Services\AttachmentThumbnailService;
|
||||
use App\Services\AuditLogger;
|
||||
use App\Models\Thread;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -115,7 +116,8 @@ class AttachmentController extends Controller
|
||||
$path = "attachments/{$scopeFolder}/{$filename}";
|
||||
Storage::disk($disk)->putFileAs("attachments/{$scopeFolder}", $file, $filename);
|
||||
|
||||
$thumbnailPayload = $this->maybeCreateThumbnail($file, $scopeFolder);
|
||||
$thumbnailPayload = app(AttachmentThumbnailService::class)
|
||||
->createForUpload($file, $scopeFolder, $disk);
|
||||
|
||||
$attachment = Attachment::create([
|
||||
'thread_id' => $threadId,
|
||||
@@ -134,6 +136,13 @@ class AttachmentController extends Controller
|
||||
'size_bytes' => (int) $file->getSize(),
|
||||
]);
|
||||
|
||||
app(AuditLogger::class)->log($request, 'attachment.created', $attachment, [
|
||||
'thread_id' => $threadId,
|
||||
'post_id' => $postId,
|
||||
'original_name' => $attachment->original_name,
|
||||
'size_bytes' => $attachment->size_bytes,
|
||||
]);
|
||||
|
||||
$attachment->loadMissing(['extension', 'group']);
|
||||
|
||||
return response()->json($this->serializeAttachment($attachment), 201);
|
||||
@@ -201,6 +210,13 @@ class AttachmentController extends Controller
|
||||
return response()->json(['message' => 'Not authorized to delete attachments.'], 403);
|
||||
}
|
||||
|
||||
app(AuditLogger::class)->log($request, 'attachment.deleted', $attachment, [
|
||||
'thread_id' => $attachment->thread_id,
|
||||
'post_id' => $attachment->post_id,
|
||||
'original_name' => $attachment->original_name,
|
||||
'size_bytes' => $attachment->size_bytes,
|
||||
]);
|
||||
|
||||
$attachment->delete();
|
||||
|
||||
return response()->json(null, 204);
|
||||
@@ -332,151 +348,4 @@ class AttachmentController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
private function maybeCreateThumbnail($file, string $scopeFolder): ?array
|
||||
{
|
||||
$enabled = $this->settingBool('attachments.create_thumbnails', true);
|
||||
if (!$enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mime = $file->getMimeType() ?? '';
|
||||
if (!str_starts_with($mime, 'image/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$maxWidth = $this->settingInt('attachments.thumbnail_max_width', 300);
|
||||
$maxHeight = $this->settingInt('attachments.thumbnail_max_height', 300);
|
||||
if ($maxWidth <= 0 || $maxHeight <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sourcePath = $file->getPathname();
|
||||
$info = @getimagesize($sourcePath);
|
||||
if (!$info) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$width, $height] = $info;
|
||||
if ($width <= 0 || $height <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($width <= $maxWidth && $height <= $maxHeight) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ratio = min($maxWidth / $width, $maxHeight / $height);
|
||||
$targetWidth = max(1, (int) round($width * $ratio));
|
||||
$targetHeight = max(1, (int) round($height * $ratio));
|
||||
|
||||
$sourceImage = $this->createImageFromFile($sourcePath, $mime);
|
||||
if (!$sourceImage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$thumbImage = imagecreatetruecolor($targetWidth, $targetHeight);
|
||||
if (!$thumbImage) {
|
||||
imagedestroy($sourceImage);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (in_array($mime, ['image/png', 'image/gif'], true)) {
|
||||
imagecolortransparent($thumbImage, imagecolorallocatealpha($thumbImage, 0, 0, 0, 127));
|
||||
imagealphablending($thumbImage, false);
|
||||
imagesavealpha($thumbImage, true);
|
||||
}
|
||||
|
||||
imagecopyresampled(
|
||||
$thumbImage,
|
||||
$sourceImage,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$targetWidth,
|
||||
$targetHeight,
|
||||
$width,
|
||||
$height
|
||||
);
|
||||
|
||||
$quality = $this->settingInt('attachments.thumbnail_quality', 85);
|
||||
$thumbBinary = $this->renderImageBinary($thumbImage, $mime, $quality);
|
||||
|
||||
imagedestroy($sourceImage);
|
||||
imagedestroy($thumbImage);
|
||||
|
||||
if ($thumbBinary === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$filename = Str::uuid()->toString();
|
||||
$extension = strtolower((string) $file->getClientOriginalExtension());
|
||||
if ($extension !== '') {
|
||||
$filename .= ".{$extension}";
|
||||
}
|
||||
|
||||
$disk = 'local';
|
||||
$thumbPath = "attachments/{$scopeFolder}/thumbs/{$filename}";
|
||||
Storage::disk($disk)->put($thumbPath, $thumbBinary);
|
||||
|
||||
return [
|
||||
'path' => $thumbPath,
|
||||
'mime' => $mime,
|
||||
'size' => strlen($thumbBinary),
|
||||
];
|
||||
}
|
||||
|
||||
private function createImageFromFile(string $path, string $mime)
|
||||
{
|
||||
return match ($mime) {
|
||||
'image/jpeg', 'image/jpg' => @imagecreatefromjpeg($path),
|
||||
'image/png' => @imagecreatefrompng($path),
|
||||
'image/gif' => @imagecreatefromgif($path),
|
||||
'image/webp' => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($path) : null,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function renderImageBinary($image, string $mime, int $quality): ?string
|
||||
{
|
||||
ob_start();
|
||||
$success = false;
|
||||
|
||||
if (in_array($mime, ['image/jpeg', 'image/jpg'], true)) {
|
||||
$success = imagejpeg($image, null, max(10, min(95, $quality)));
|
||||
} elseif ($mime === 'image/png') {
|
||||
$compression = (int) round(9 - (max(10, min(95, $quality)) / 100) * 9);
|
||||
$success = imagepng($image, null, $compression);
|
||||
} elseif ($mime === 'image/gif') {
|
||||
$success = imagegif($image);
|
||||
} elseif ($mime === 'image/webp' && function_exists('imagewebp')) {
|
||||
$success = imagewebp($image, null, max(10, min(95, $quality)));
|
||||
}
|
||||
|
||||
$data = ob_get_clean();
|
||||
|
||||
if (!$success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data !== false ? $data : null;
|
||||
}
|
||||
|
||||
private function settingBool(string $key, bool $default): bool
|
||||
{
|
||||
$value = Setting::query()->where('key', $key)->value('value');
|
||||
if ($value === null) {
|
||||
return $default;
|
||||
}
|
||||
return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true);
|
||||
}
|
||||
|
||||
private function settingInt(string $key, int $default): int
|
||||
{
|
||||
$value = Setting::query()->where('key', $key)->value('value');
|
||||
if ($value === null) {
|
||||
return $default;
|
||||
}
|
||||
return (int) $value;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user