disk = Storage::disk('s3'); } /** * Upload member profile image * * @param UploadedFile $imageFile * @param int $memberId * @return string * @throws Exception */ public function uploadProfileImage(UploadedFile $imageFile, int $memberId): string { try { $currentClient = session('currentClient', 'default'); // Validate file $this->validateFile($imageFile, 'image'); // Delete existing profile image $this->deleteExistingProfileImage($memberId); // Create new filename $extension = strtolower($imageFile->getClientOriginalExtension()); $filename = 'profile_' . $memberId . '_' . time() . '.' . $extension; // Upload to S3 $s3Path = $currentClient . '/members/' . $memberId . '/images/' . $filename; $uploaded = $this->disk->putFileAs( $currentClient . '/members/' . $memberId . '/images', $imageFile, $filename, 'private' ); if (!$uploaded) { throw new Exception('Failed to upload profile image to S3'); } Log::info("Profile image uploaded", [ 'member_id' => $memberId, 'path' => $s3Path, 'size' => $imageFile->getSize() ]); return $s3Path; } catch (Exception $e) { Log::error("Error uploading profile image", [ 'member_id' => $memberId, 'error' => $e->getMessage() ]); throw $e; } } /** * Upload single document file * * @param UploadedFile $documentFile * @param int $memberId * @param string $documentType ('self', 'father', 'mother') * @return string * @throws Exception */ public function uploadDocument(UploadedFile $documentFile, int $memberId, string $documentType = 'self'): string { try { $currentClient = session('currentClient', 'default'); // Validate file $this->validateFile($documentFile, 'document'); // Create filename $originalName = pathinfo($documentFile->getClientOriginalName(), PATHINFO_FILENAME); $extension = strtolower($documentFile->getClientOriginalExtension()); $filename = $documentType . '_' . $originalName . '_' . time() . '.' . $extension; // Upload to S3 $s3Path = $currentClient . '/members/' . $memberId . '/documents/' . $filename; $uploaded = $this->disk->putFileAs( $currentClient . '/members/' . $memberId . '/documents', $documentFile, $filename, 'private' ); if (!$uploaded) { throw new Exception('Failed to upload document to S3: ' . $originalName); } Log::info("Document uploaded", [ 'member_id' => $memberId, 'type' => $documentType, 'path' => $s3Path, 'original_name' => $documentFile->getClientOriginalName() ]); return $s3Path; } catch (Exception $e) { Log::error("Error uploading document", [ 'member_id' => $memberId, 'type' => $documentType, 'error' => $e->getMessage() ]); throw $e; } } /** * Upload certificate file * * @param UploadedFile $certificateFile * @param int $memberId * @return string * @throws Exception */ public function uploadCertificate(UploadedFile $certificateFile, int $memberId): string { try { $currentClient = session('currentClient', 'default'); // Validate file $this->validateFile($certificateFile, 'document'); // Create filename $extension = strtolower($certificateFile->getClientOriginalExtension()); $filename = 'certificate_' . $memberId . '_' . time() . '.' . $extension; // Upload to S3 $s3Path = $currentClient . '/members/' . $memberId . '/certificates/' . $filename; $uploaded = $this->disk->putFileAs( $currentClient . '/members/' . $memberId . '/certificates', $certificateFile, $filename, 'private' ); if (!$uploaded) { throw new Exception('Failed to upload certificate to S3'); } Log::info("Certificate uploaded", [ 'member_id' => $memberId, 'path' => $s3Path, 'size' => $certificateFile->getSize() ]); return $s3Path; } catch (Exception $e) { Log::error("Error uploading certificate", [ 'member_id' => $memberId, 'error' => $e->getMessage() ]); throw $e; } } /** * Get file URL for display * * @param string $filePath * @param string $expiresIn * @return string|null */ public function getFileUrl(string $filePath, string $expiresIn = '+1 hour'): ?string { if (!$filePath) { return null; } // Handle legacy local paths - return asset URL if (!$this->isS3Path($filePath)) { return asset('storage/' . $filePath); } try { if (!$this->disk->exists($filePath)) { Log::warning("File not found", ['path' => $filePath]); return null; } return $this->disk->temporaryUrl($filePath, now()->add($expiresIn)); } catch (Exception $e) { Log::error("Error generating file URL", [ 'path' => $filePath, 'error' => $e->getMessage() ]); return null; } } /** * Delete file from S3 * * @param string $filePath * @return bool */ public function deleteFile(string $filePath): bool { // Don't try to delete local files if (!$this->isS3Path($filePath)) { return false; } try { if ($this->disk->exists($filePath)) { $this->disk->delete($filePath); Log::info("File deleted", ['path' => $filePath]); return true; } return false; } catch (Exception $e) { Log::error("Error deleting file", [ 'path' => $filePath, 'error' => $e->getMessage() ]); return false; } } /** * Check if path is S3 path */ private function isS3Path(string $path): bool { return strpos($path, '/members/') !== false || strpos($path, session('currentClient', 'default')) === 0; } /** * Delete existing profile image for member * * @param int $memberId */ private function deleteExistingProfileImage(int $memberId): void { try { $currentClient = session('currentClient', 'default'); $imagePath = $currentClient . '/members/' . $memberId . '/images/'; // List all files in the member's image directory $files = $this->disk->files($imagePath); foreach ($files as $file) { if (strpos(basename($file), 'profile_' . $memberId) === 0) { $this->disk->delete($file); Log::info("Deleted existing profile image", [ 'member_id' => $memberId, 'file' => $file ]); } } } catch (Exception $e) { Log::warning("Error deleting existing profile image", [ 'member_id' => $memberId, 'error' => $e->getMessage() ]); } } /** * Validate uploaded file * * @param UploadedFile $file * @param string $type ('image' or 'document') * @throws Exception */ private function validateFile(UploadedFile $file, string $type): void { // Check if file is valid if (!$file->isValid()) { throw new Exception('Invalid file upload'); } $extension = strtolower($file->getClientOriginalExtension()); $allowedExtensions = $type === 'image' ? self::ALLOWED_IMAGE_EXTENSIONS : self::ALLOWED_DOCUMENT_EXTENSIONS; $maxSize = $type === 'image' ? self::MAX_IMAGE_SIZE : self::MAX_DOCUMENT_SIZE; // Check file extension if (!in_array($extension, $allowedExtensions)) { throw new Exception("File type not allowed. Allowed types: " . implode(', ', $allowedExtensions)); } // Check file size if ($file->getSize() > ($maxSize * 1024)) { throw new Exception("File size exceeds maximum allowed size of {$maxSize}KB"); } // Check mime type for images if ($type === 'image') { $allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif']; if (!in_array($file->getMimeType(), $allowedMimeTypes)) { throw new Exception('Invalid image file type'); } } } /** * Create member folder structure in S3 * * @param int $memberId */ public function createMemberFolders(int $memberId): void { try { $currentClient = session('currentClient', 'default'); $folders = [ $currentClient . '/members/' . $memberId . '/images/.gitkeep', $currentClient . '/members/' . $memberId . '/documents/.gitkeep', $currentClient . '/members/' . $memberId . '/certificates/.gitkeep' ]; foreach ($folders as $folder) { if (!$this->disk->exists($folder)) { $this->disk->put($folder, ''); } } Log::info("Created member folder structure", ['member_id' => $memberId]); } catch (Exception $e) { Log::error("Error creating member folders", [ 'member_id' => $memberId, 'error' => $e->getMessage() ]); } } /** * Get file info * * @param string $filePath * @return array|null */ public function getFileInfo(string $filePath): ?array { if (!$filePath) { return null; } // Handle legacy local paths if (!$this->isS3Path($filePath)) { $localPath = storage_path('app/public/' . $filePath); if (file_exists($localPath)) { return [ 'path' => $filePath, 'name' => basename($filePath), 'size' => filesize($localPath), 'last_modified' => filemtime($localPath), 'url' => $this->getFileUrl($filePath), 'exists' => true, 'storage_type' => 'local' ]; } return null; } if (!$this->disk->exists($filePath)) { return null; } try { return [ 'path' => $filePath, 'name' => basename($filePath), 'size' => $this->disk->size($filePath), 'last_modified' => $this->disk->lastModified($filePath), 'url' => $this->getFileUrl($filePath), 'exists' => true, 'storage_type' => 's3' ]; } catch (Exception $e) { Log::error("Error getting file info", [ 'path' => $filePath, 'error' => $e->getMessage() ]); return null; } } }