Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.64% covered (warning)
88.64%
39 / 44
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
PortalDocumentStorage
88.64% covered (warning)
88.64%
39 / 44
25.00% covered (danger)
25.00%
1 / 4
19.53
0.00% covered (danger)
0.00%
0 / 1
 disk
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 store
96.15% covered (success)
96.15%
25 / 26
0.00% covered (danger)
0.00%
0 / 1
6
 temporaryUrl
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 assertAllowed
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
9.37
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\ClientPortal\Application\Services;
6
7use Illuminate\Http\UploadedFile;
8use Illuminate\Support\Facades\Storage;
9use Illuminate\Support\Str;
10use ImovelZapAi\ClientPortal\Domain\Exceptions\ClientPortalException;
11
12final class PortalDocumentStorage
13{
14    public function disk(): string
15    {
16        return (string) config('client_portal.disk', 'local');
17    }
18
19    /**
20     * @return array{disk: string, path: string, original_name: string, mime: string|null, size_bytes: int, checksum: string}
21     */
22    public function store(string $tenantId, string $portalId, string $requestedDocumentId, UploadedFile $file): array
23    {
24        $this->assertAllowed($file);
25
26        $ext = strtolower($file->getClientOriginalExtension() ?: $file->extension() ?: 'bin');
27        $path = sprintf(
28            'client-portals/%s/%s/%s/%s.%s',
29            $tenantId,
30            $portalId,
31            $requestedDocumentId,
32            Str::uuid()->toString(),
33            $ext,
34        );
35
36        $stored = Storage::disk($this->disk())->putFileAs(
37            dirname($path),
38            $file,
39            basename($path),
40        );
41
42        if ($stored === false) {
43            throw ClientPortalException::uploadRejected('falha ao gravar arquivo.');
44        }
45
46        $checksum = hash_file('sha256', $file->getRealPath() ?: $file->getPathname()) ?: '';
47
48        return [
49            'disk' => $this->disk(),
50            'path' => $path,
51            'original_name' => $file->getClientOriginalName(),
52            'mime' => $file->getMimeType(),
53            'size_bytes' => (int) $file->getSize(),
54            'checksum' => $checksum,
55        ];
56    }
57
58    public function temporaryUrl(string $disk, string $path, int $minutes = 15): ?string
59    {
60        $filesystem = Storage::disk($disk);
61        if (method_exists($filesystem, 'temporaryUrl')) {
62            try {
63                return $filesystem->temporaryUrl($path, now()->addMinutes($minutes));
64            } catch (\Throwable) {
65                // local disk may not support temporary URLs
66            }
67        }
68
69        return null;
70    }
71
72    private function assertAllowed(UploadedFile $file): void
73    {
74        $maxKb = (int) config('client_portal.max_upload_kb', 10240);
75        if ($file->getSize() > $maxKb * 1024) {
76            throw ClientPortalException::uploadRejected("arquivo maior que {$maxKb} KB.");
77        }
78
79        $ext = strtolower($file->getClientOriginalExtension() ?: '');
80        $allowedExt = config('client_portal.allowed_extensions', []);
81        if ($ext !== '' && ! in_array($ext, $allowedExt, true)) {
82            throw ClientPortalException::uploadRejected('tipo de arquivo não permitido.');
83        }
84
85        $mime = (string) $file->getMimeType();
86        $allowedMimes = config('client_portal.allowed_mimes', []);
87        if ($mime !== '' && $allowedMimes !== [] && ! in_array($mime, $allowedMimes, true)) {
88            // HEIC sometimes reported as application/octet-stream
89            if (! in_array($ext, ['heic', 'heif'], true)) {
90                throw ClientPortalException::uploadRejected('MIME não permitido.');
91            }
92        }
93    }
94}