normalizeDirectory($path); if ($normalizedPath === '') { throw new AttachmentStorageException('The attachment path cannot be empty.'); } $key = (string) Str::uuid(); $fileData = $this->resolveFileData($file, $key); $storedPath = $this->storeFile( $fileData, $normalizedPath, $this->buildStoredFilename($key, $fileData['extension']), ); if (! is_string($storedPath) || $storedPath === '') { throw new AttachmentStorageException('No se pudo subir el archivo al disco s3.'); } try { /** @var Attachment $attachment */ $attachment = Attachment::query()->create([ 'key' => $key, 'path' => $storedPath, 'filename' => $this->resolveFilename($file, $key), 'type' => $this->resolveAttachmentType($fileData['mime_type']), 'mime_type' => $fileData['mime_type'], 'extension' => $fileData['extension'], 'size' => $fileData['size'], ]); return $attachment; } catch (Throwable $throwable) { Storage::disk('s3')->delete($storedPath); throw $throwable; } } public function delete(Attachment $attachment): void { $deleted = Storage::disk('s3')->delete($attachment->path); if (! $deleted) { throw new AttachmentStorageException('No se pudo eliminar el archivo del disco s3.'); } $attachment->delete(); } protected function normalizeDirectory(string $path): string { return trim($path, '/'); } protected function resolveFilename(UploadedFile|string $file, string $key): string { if ($file instanceof UploadedFile) { $originalName = trim($file->getClientOriginalName()); if ($originalName !== '') { return $originalName; } } return $key; } /** * @return array{contents: string|null, extension: string, file: UploadedFile|null, mime_type: string, size: int} */ protected function resolveFileData(UploadedFile|string $file, string $filename): array { if ($file instanceof UploadedFile) { return $this->fileDataFromUploadedFile($file); } return $this->fileDataFromBase64($file, $filename); } /** * @return array{contents: string|null, extension: string, file: UploadedFile, mime_type: string, size: int} */ protected function fileDataFromUploadedFile(UploadedFile $file): array { $mimeType = strtolower($file->getClientMimeType() ?? $file->getMimeType() ?? 'application/octet-stream'); return [ 'contents' => null, 'extension' => strtolower($file->getClientOriginalExtension() ?: $file->extension() ?: ''), 'file' => $file, 'mime_type' => $mimeType, 'size' => $file->getSize() ?? 0, ]; } /** * @return array{contents: string, extension: string, file: null, mime_type: string, size: int} */ protected function fileDataFromBase64(string $file, string $filename): array { ['data' => $contents, 'mime_type' => $declaredMimeType] = $this->decodeBase64File($file); $mimeType = $this->detectMimeType($contents, $declaredMimeType); return [ 'contents' => $contents, 'extension' => $this->resolveExtension($filename, $mimeType), 'file' => null, 'mime_type' => $mimeType, 'size' => strlen($contents), ]; } /** * @return array{data: string, mime_type: string|null} */ protected function decodeBase64File(string $file): array { $payload = trim($file); if ($payload === '') { throw new AttachmentStorageException('The base64 attachment content cannot be empty.'); } $declaredMimeType = null; if (preg_match('/^data:(?[-\w.+\/]+);base64,(?.+)$/s', $payload, $matches) === 1) { $declaredMimeType = strtolower($matches['mime']); $payload = $matches['data']; } $decoded = base64_decode(preg_replace('/\s+/', '', $payload), true); if ($decoded === false || $decoded === '') { throw new AttachmentStorageException('The attachment base64 payload is invalid.'); } return [ 'data' => $decoded, 'mime_type' => $declaredMimeType, ]; } protected function detectMimeType(string $contents, ?string $fallback = null): string { if (is_string($fallback) && $fallback !== '') { return strtolower($fallback); } $detectedMimeType = (new \finfo(FILEINFO_MIME_TYPE))->buffer($contents); if (is_string($detectedMimeType) && $detectedMimeType !== '') { return strtolower($detectedMimeType); } return strtolower($fallback ?? 'application/octet-stream'); } protected function resolveExtension(string $filename, string $mimeType): string { $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); if ($extension !== '') { return $extension; } return strtolower(MimeTypes::getDefault()->getExtensions($mimeType)[0] ?? ''); } /** * @param array{contents: string|null, extension: string, file: UploadedFile|null, mime_type: string, size: int} $fileData */ protected function storeFile(array $fileData, string $directory, string $storedFilename): string { if ($fileData['file'] instanceof UploadedFile) { return Storage::disk('s3')->putFileAs($directory, $fileData['file'], $storedFilename); } $storedPath = $directory !== '' ? $directory.'/'.$storedFilename : $storedFilename; $stored = Storage::disk('s3')->put($storedPath, $fileData['contents'] ?? ''); if (! $stored) { return ''; } return $storedPath; } protected function resolveAttachmentType(string $mimeType): AttachmentType { $mimeType = strtolower($mimeType); if (str_starts_with($mimeType, 'image/')) { return AttachmentType::Image; } if (str_starts_with($mimeType, 'video/')) { return AttachmentType::Video; } if ($mimeType === 'application/pdf') { return AttachmentType::Pdf; } if (str_starts_with($mimeType, 'audio/')) { return AttachmentType::Audio; } if ( str_starts_with($mimeType, 'text/') || str_contains($mimeType, 'document') || str_contains($mimeType, 'word') || str_contains($mimeType, 'excel') || str_contains($mimeType, 'spreadsheet') || str_contains($mimeType, 'presentation') || str_contains($mimeType, 'officedocument') ) { return AttachmentType::Document; } return AttachmentType::Other; } protected function buildStoredFilename(string $key, string $extension): string { if ($extension === '') { return $key; } return $key.'.'.$extension; } }