$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: $ticket, comment: $comment, userId: $userId, disk: $disk, path: $path, originalName: $originalName, mimeType: $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 */ private function attachmentAttributes(Ticket $ticket, ?Comment $comment, int $userId, string $disk, string $path, string $originalName, string $mimeType, int $size): array { $attributes = [ 'ticket_id' => $ticket->id, 'comment_id' => $comment?->id, 'user_id' => $userId, 'disk' => $disk, 'path' => $path, 'original_name' => $originalName, 'mime_type' => $mimeType, 'size' => $size, ]; if (Schema::hasColumn('helpdesk_attachments', 'extension')) { $attributes['extension'] = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION)); } if (Schema::hasColumn('helpdesk_attachments', 'category')) { $attributes['category'] = $this->categoryFor($originalName, $mimeType); } if (Schema::hasColumn('helpdesk_attachments', 'checksum_sha256') && Storage::disk($disk)->exists($path)) { $stream = Storage::disk($disk)->readStream($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)); if (str_starts_with($mimeType, 'image/') || in_array($extension, ['png', 'jpg', 'jpeg', 'webp', 'gif'], true)) { return 'image'; } if ($extension === 'pdf' || $mimeType === 'application/pdf') { return 'pdf'; } if (in_array($extension, ['zip', 'tar', 'gz', '7z', 'rar'], true)) { return 'archive'; } if (in_array($extension, config('helpdesk.allowed_source_extensions', []), true)) { return 'source'; } return '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); } }