172 lines
5.7 KiB
PHP
Executable File
172 lines
5.7 KiB
PHP
Executable File
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\Helpdesk\Services;
|
|
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
|
use Modules\Helpdesk\Models\Attachment;
|
|
use Modules\Helpdesk\Models\Comment;
|
|
use Modules\Helpdesk\Models\Ticket;
|
|
|
|
final class AttachmentService
|
|
{
|
|
/**
|
|
* Store multiple Helpdesk attachments against a ticket or comment.
|
|
*
|
|
* Livewire create/comment forms pass TemporaryUploadedFile instances, while
|
|
* controller endpoints pass Symfony/Laravel UploadedFile instances. Both
|
|
* are supported here so uploads are never silently skipped on ticket create.
|
|
*
|
|
* @param array<int|string, mixed> $files
|
|
*/
|
|
public function storeMany(Ticket $ticket, array $files, int $userId, ?Comment $comment = null): void
|
|
{
|
|
foreach ($files as $file) {
|
|
if (! $this->isSupportedUpload($file)) {
|
|
continue;
|
|
}
|
|
|
|
$disk = (string) config('helpdesk.disk', 'public');
|
|
$originalName = $this->originalName($file);
|
|
$mimeType = $this->mimeType($file);
|
|
$size = $this->size($file);
|
|
$path = $file->store('tickets/' . $ticket->id, $disk);
|
|
|
|
if (! is_string($path) || $path === '') {
|
|
continue;
|
|
}
|
|
|
|
if ($size === 0) {
|
|
$size = (int) Storage::disk($disk)->size($path);
|
|
}
|
|
|
|
Attachment::query()->create($this->attachmentAttributes($ticket, $comment, $userId, [
|
|
'disk' => $disk,
|
|
'path' => $path,
|
|
'original_name' => $originalName,
|
|
'mime_type' => $mimeType,
|
|
'size' => $size,
|
|
]));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a stored attachment and its backing file.
|
|
*/
|
|
public function delete(Attachment $attachment): void
|
|
{
|
|
Storage::disk($attachment->disk)->delete($attachment->path);
|
|
$attachment->delete();
|
|
}
|
|
|
|
/**
|
|
* Build attachment attributes while remaining compatible with older databases.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function attachmentAttributes(Ticket $ticket, ?Comment $comment, int $userId, array $file): array
|
|
{
|
|
$attributes = [
|
|
'ticket_id' => $ticket->id,
|
|
'comment_id' => $comment?->id,
|
|
'user_id' => $userId,
|
|
'disk' => $file['disk'],
|
|
'path' => $file['path'],
|
|
'original_name' => $file['original_name'],
|
|
'mime_type' => $file['mime_type'],
|
|
'size' => $file['size'],
|
|
];
|
|
|
|
if (Schema::hasColumn('helpdesk_attachments', 'extension')) {
|
|
$attributes['extension'] = strtolower((string) pathinfo($file['original_name'], PATHINFO_EXTENSION));
|
|
}
|
|
|
|
if (Schema::hasColumn('helpdesk_attachments', 'category')) {
|
|
$attributes['category'] = $this->categoryFor($file['original_name'], $file['mime_type']);
|
|
}
|
|
|
|
if (Schema::hasColumn('helpdesk_attachments', 'checksum_sha256') && Storage::disk($file['disk'])->exists($file['path'])) {
|
|
$stream = Storage::disk($file['disk'])->readStream($file['path']);
|
|
|
|
if (is_resource($stream)) {
|
|
$context = hash_init('sha256');
|
|
hash_update_stream($context, $stream);
|
|
$attributes['checksum_sha256'] = hash_final($context);
|
|
fclose($stream);
|
|
}
|
|
}
|
|
|
|
return $attributes;
|
|
}
|
|
|
|
/**
|
|
* Categorise uploaded files for icons, previews and safe code viewing.
|
|
*/
|
|
private function categoryFor(string $originalName, string $mimeType): string
|
|
{
|
|
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
|
|
|
|
return match (true) {
|
|
str_starts_with($mimeType, 'image/'), in_array($extension, ['png', 'jpg', 'jpeg', 'webp', 'gif'], true) => 'image',
|
|
$extension === 'pdf', $mimeType === 'application/pdf' => 'pdf',
|
|
in_array($extension, ['zip', 'tar', 'gz', '7z', 'rar'], true) => 'archive',
|
|
in_array($extension, config('helpdesk.allowed_source_extensions', []), true) => 'source',
|
|
default => 'file',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Soft-delete a stored attachment while retaining audit metadata.
|
|
*/
|
|
public function softDelete(Attachment $attachment, int $userId): void
|
|
{
|
|
DB::table('helpdesk_attachments')
|
|
->where('id', $attachment->id)
|
|
->update(array_filter([
|
|
'deleted_at' => now(),
|
|
'deleted_by' => Schema::hasColumn('helpdesk_attachments', 'deleted_by') ? $userId : null,
|
|
], static fn (mixed $value): bool => $value !== null));
|
|
}
|
|
|
|
/**
|
|
* Determine whether the value is a storeable uploaded file.
|
|
*/
|
|
private function isSupportedUpload(mixed $file): bool
|
|
{
|
|
if ($file instanceof TemporaryUploadedFile) {
|
|
return true;
|
|
}
|
|
|
|
return $file instanceof UploadedFile && $file->isValid();
|
|
}
|
|
|
|
/**
|
|
* Return the uploaded file's original client filename.
|
|
*/
|
|
private function originalName(mixed $file): string
|
|
{
|
|
return (string) $file->getClientOriginalName();
|
|
}
|
|
|
|
/**
|
|
* Return the uploaded file's reported mime type.
|
|
*/
|
|
private function mimeType(mixed $file): string
|
|
{
|
|
return (string) ($file->getClientMimeType() ?: $file->getMimeType() ?: 'application/octet-stream');
|
|
}
|
|
|
|
/**
|
|
* Return the uploaded file's byte size.
|
|
*/
|
|
private function size(mixed $file): int
|
|
{
|
|
return (int) ($file->getSize() ?: 0);
|
|
}
|
|
}
|